├── .gitignore ├── Gemfile ├── .rspec ├── lib ├── dory.rb └── dory │ ├── version.rb │ ├── upgrade.rb │ ├── resolv │ ├── linux_resolvconf.rb │ ├── linux.rb │ └── macos.rb │ ├── resolv.rb │ ├── dinghy.rb │ ├── port_utils.rb │ ├── shell.rb │ ├── os.rb │ ├── systemd.rb │ ├── proxy.rb │ ├── docker_service.rb │ ├── config.rb │ └── dnsmasq.rb ├── Rakefile ├── travis ├── install-dependencies.sh └── clear-port-53.sh ├── spec ├── lib │ ├── version_spec.rb │ ├── os_spec.rb │ ├── resolv_spec.rb │ ├── port_utils_spec.rb │ ├── resolv │ │ ├── linux_resolvconf_spec.rb │ │ ├── linux_spec.rb │ │ └── macos_spec.rb │ ├── dinghy_spec.rb │ ├── upgrade_spec.rb │ ├── proxy_spec.rb │ ├── config_spec.rb │ ├── systemd_spec.rb │ └── dnsmasq_spec.rb ├── spec_helper.rb └── bin │ └── dory_spec.rb ├── .codeclimate.yml ├── .travis.yml ├── LICENSE ├── dory.gemspec ├── Gemfile.lock ├── bin └── dory ├── README.md └── .rubocop.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/dory.rb: -------------------------------------------------------------------------------- 1 | Gem.find_files('dory/**/*.rb').each do |path| 2 | require path.gsub(/\.rb$/, '') unless path =~ /bot.*cli/ 3 | end 4 | -------------------------------------------------------------------------------- /lib/dory/version.rb: -------------------------------------------------------------------------------- 1 | module Dory 2 | def self.version 3 | '1.2.0' 4 | end 5 | 6 | def self.date 7 | '2022-09-21' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | task test: :spec 8 | -------------------------------------------------------------------------------- /travis/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo apt-get update 4 | sudo apt-get -y -o Dpkg::Options::='--force-confnew' install docker.io openssl libssl-dev nmap 5 | 6 | gem install bundler 7 | bundle install 8 | -------------------------------------------------------------------------------- /spec/lib/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Dory do 4 | it 'has a version number' do 5 | expect(Dory.version).not_to be_nil 6 | end 7 | 8 | it 'has a date' do 9 | expect(Dory.date).not_to be_nil 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lib/os_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::Os do 2 | it "knows we're on ubuntu" do 3 | expect(Dory::Os.ubuntu?).to be_truthy 4 | expect(Dory::Os.fedora?).to be_falsey 5 | expect(Dory::Os.arch?).to be_falsey 6 | expect(Dory::Os.macos?).to be_falsey 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | bundler-audit: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - ruby 10 | fixme: 11 | enabled: true 12 | rubocop: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - Gemfile.lock 17 | - "**.rb" 18 | exclude_paths: 19 | - spec/ 20 | -------------------------------------------------------------------------------- /travis/clear-port-53.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Checking for listeners on port 53" 4 | 5 | # There's a dnsmasq container listening under user lxc-dnsmasq. 6 | # This will interfere with our test 7 | for i in $(sudo lsof -i :53 | awk '{print $2}' | tail -n +2 | xargs); do 8 | echo "" 9 | sleep 1 10 | echo "Killing PID $i cause it's listening on port 53" 11 | echo "PID $i info:" 12 | sudo ps -p $i -o user -o command 13 | sudo kill $i 14 | echo "" 15 | done 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | #- "2.1.10" # not supported anymore. byebug breaks 4 | - "2.2.7" 5 | - "2.3.4" 6 | - "2.4.4" 7 | - "2.5.1" 8 | - "ruby-head" 9 | #- rbx-2 10 | #- rbx-3 11 | #- jruby-9.0.0.0 12 | #- jruby-9.0.1.0 13 | #- jruby-9.0.4.0 14 | #- jruby-9.0.5.0 15 | #- jruby-head 16 | sudo: required 17 | dist: trusty 18 | before_install: gem update --system 19 | install: ./travis/install-dependencies.sh 20 | before_script: ./travis/clear-port-53.sh 21 | script: bundle exec rspec spec 22 | addons: 23 | code_climate: 24 | repo_token: d8cb42815dd1076748cb239c298cd700d96c5a1b4aefa7ae340c6701050b6432 25 | -------------------------------------------------------------------------------- /lib/dory/upgrade.rb: -------------------------------------------------------------------------------- 1 | module Dory 2 | module Upgrade 3 | def self.new_version 4 | res = Dory::Sh.run_command('gem search -q dory') 5 | return false unless res.success? 6 | newver = /dory\s+\((.*)\)/.match(res.stdout) 7 | return false if !newver || newver.length != 2 8 | newver[1] 9 | end 10 | 11 | def self.outdated?(new_version = self.new_version) 12 | return Dory.version != new_version 13 | end 14 | 15 | def self.install 16 | Dory::Sh.run_command('gem install dory') 17 | end 18 | 19 | def self.cleanup 20 | Dory::Sh.run_command('gem cleanup dory') 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dory/resolv/linux_resolvconf.rb: -------------------------------------------------------------------------------- 1 | module Dory 2 | module Resolv 3 | module LinuxResolvconf 4 | def self.file_nameserver_line 5 | Linux.file_nameserver_line 6 | end 7 | 8 | def self.nameserver_contents 9 | Linux.nameserver_contents 10 | end 11 | 12 | def self.has_our_nameserver? 13 | Linux.has_our_nameserver? 14 | end 15 | 16 | def self.configure 17 | puts 'Requesting sudo to run resolvconf'.green 18 | Bash.run_command("echo -e '#{self.nameserver_contents}' | sudo resolvconf -a lo.dory") 19 | end 20 | 21 | def self.clean 22 | puts 'Requesting sudo to run resolvconf'.green 23 | Bash.run_command("sudo resolvconf -d lo.dory") 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/resolv_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::Resolv do 2 | let(:modules) do 3 | [ 4 | Dory::Resolv::Linux, 5 | Dory::Resolv::LinuxResolvconf, 6 | Dory::Resolv::Macos 7 | ] 8 | end 9 | let(:methods) do 10 | %i[has_our_nameserver? configure clean file_nameserver_line] 11 | end 12 | 13 | it 'calls the versions based on platform' do 14 | modules.each do |platform| 15 | allow(Dory::Os).to receive(:macos?) { platform == Dory::Resolv::Macos } 16 | allow(Dory::Resolv).to receive(:resolvconf?) { platform == Dory::Resolv::LinuxResolvconf } 17 | methods.each do |m| 18 | allow(platform).to receive(m) 19 | Dory::Resolv.send(m) 20 | expect(platform).to have_received(m) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/dory/resolv.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | require 'pathname' 3 | 4 | module Dory 5 | module Resolv 6 | def self.get_module 7 | return Dory::Resolv::Macos if Os.macos? 8 | return Dory::Resolv::LinuxResolvconf if self.resolvconf? 9 | Dory::Resolv::Linux 10 | end 11 | 12 | def self.has_our_nameserver? 13 | self.get_module.has_our_nameserver? 14 | end 15 | 16 | def self.configure 17 | self.get_module.configure 18 | end 19 | 20 | def self.file_nameserver_line 21 | self.get_module.file_nameserver_line 22 | end 23 | 24 | def self.clean 25 | self.get_module.clean 26 | end 27 | 28 | def self.resolvconf? 29 | Pathname.new('/etc/resolv.conf').realpath.to_s == 30 | '/run/resolvconf/resolv.conf' 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/port_utils_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::PortUtils do 2 | def start_service_on_53 3 | puts "Requesting sudo to start an ncat listener on 53" 4 | `sudo echo 'Got sudo. starting ncat listener'` 5 | Process.spawn('sudo ncat -l 53') 6 | sleep(0.5) # give the process time to bind 7 | end 8 | 9 | def cleanup_53 10 | Dory::Sh.run_command('sudo killall ncat') 11 | Dory::Sh.run_command('sudo killall exe') 12 | end 13 | 14 | describe '#check_port' do 15 | let(:port) { 53 } 16 | 17 | before(:each) { start_service_on_53 } 18 | after(:each) { cleanup_53 } 19 | 20 | it "detects listening services" do 21 | expect(Dory::PortUtils.check_port(port).count).to eq(2) 22 | Dory::PortUtils.check_port(port).each { |p| expect(p.command).to match(/^(ncat|exe)$/) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dory/dinghy.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Dory 4 | module Dinghy 5 | class DinghyError < RuntimeError 6 | end 7 | 8 | def self.installed? 9 | Bash.run_command("which dinghy >/dev/null 2>&1").success? 10 | end 11 | 12 | def self.ip 13 | res = Bash.run_command("dinghy ip").stdout.chomp 14 | unless res =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/ 15 | raise DinghyError.new(<<-ERROR) 16 | Dinghy responded with: '#{res}', but we expected an IP address. 17 | Please make sure the dinghy vm is running, and that running 18 | `dinghy ip` gives you an IP address 19 | ERROR 20 | end 21 | res 22 | end 23 | 24 | def self.match?(str) 25 | # be lenient cause I typo this all the time 26 | str =~ /^:?din.?.?y:?/ 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/dory/port_utils.rb: -------------------------------------------------------------------------------- 1 | require_relative 'docker_service' 2 | 3 | module Dory 4 | module PortUtils 5 | def self.check_port(port_num = self.port) 6 | puts "Requesting sudo to check if something is bound to port #{port_num}".green 7 | ret = Dory::Sh.run_command("sudo lsof -i :#{port_num}") 8 | return [] unless ret.success? 9 | 10 | list = ret.stdout.split("\n") 11 | list.shift # get rid of the column headers 12 | list.map! do |process| 13 | command, pid, user, fd, type, device, size, node, name = process.split(/\s+/) 14 | OpenStruct.new({ 15 | command: command, 16 | pid: pid, 17 | user: user, 18 | fd: fd, 19 | type: type, 20 | device: device, 21 | size: size, 22 | node: node, 23 | name: name 24 | }) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/dory/shell.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Dory 4 | module Sh 5 | def self.run_command(command) 6 | stdout = `#{command}` 7 | OpenStruct.new({ 8 | success?: $?.exitstatus == 0, 9 | exitstatus: $?.exitstatus, 10 | stdout: stdout 11 | }) 12 | end 13 | end 14 | 15 | module Bash 16 | def self.escape_single_quotes(str) 17 | # Using this technique here: https://stackoverflow.com/a/1315213/2062384 18 | str.gsub("'", "'\\\\''") 19 | end 20 | 21 | def self.escape_double_quotes(str) 22 | str.gsub('"', '\\"') 23 | end 24 | 25 | def self.run_command(command) 26 | stdout = `bash -c "#{self.escape_double_quotes(command)}"` 27 | OpenStruct.new({ 28 | success?: $?.exitstatus == 0, 29 | exitstatus: $?.exitstatus, 30 | stdout: stdout 31 | }) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/resolv/linux_resolvconf_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | RSpec.describe Dory::Resolv::LinuxResolvconf do 4 | describe '.file_nameserver_line' do 5 | it 'delegates to the linux module' 6 | end 7 | 8 | describe '.nameserver_contents' do 9 | it 'delegates to the linux module' 10 | end 11 | 12 | describe '.has_our_nameserver?' do 13 | it 'delegates to the linux module' 14 | end 15 | 16 | describe '.configure' do 17 | before do 18 | allow(Dory::Bash).to receive(:run_command) 19 | end 20 | 21 | it 'runs resolvconf' do 22 | Dory::Resolv::LinuxResolvconf.configure 23 | expect(Dory::Bash).to have_received(:run_command).with( 24 | "echo -e 'nameserver 127.0.0.1 # added by dory' | sudo resolvconf -a lo.dory" 25 | ) 26 | end 27 | end 28 | 29 | describe '.clean' do 30 | before do 31 | allow(Dory::Bash).to receive(:run_command) 32 | end 33 | 34 | it 'runs resolvconf' do 35 | Dory::Resolv::LinuxResolvconf.clean 36 | expect(Dory::Bash).to have_received(:run_command).with( 37 | 'sudo resolvconf -d lo.dory' 38 | ) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Benjamin Porter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/dory/os.rb: -------------------------------------------------------------------------------- 1 | module Dory 2 | module Os 3 | def self.bash(command) 4 | system("bash -c '#{command}'") 5 | end 6 | 7 | def self.ubuntu? 8 | self.bash(self.ubuntu_cmd) 9 | end 10 | 11 | def self.fedora? 12 | self.bash(self.fedora_cmd) 13 | end 14 | 15 | def self.arch? 16 | self.bash(self.arch_cmd) 17 | end 18 | 19 | def self.macos? 20 | self.bash('uname -a | grep "Darwin" > /dev/null') 21 | end 22 | 23 | def self.ubuntu_cmd 24 | %q( 25 | if $(which lsb_release >/dev/null 2>&1); then 26 | lsb_release -d | grep --color=auto "Ubuntu" > /dev/null 27 | else 28 | uname -a | grep --color=auto "Ubuntu" > /dev/null 29 | fi 30 | ) 31 | end 32 | 33 | def self.fedora_cmd 34 | %q( 35 | if $(which lsb_release >/dev/null 2>&1); then 36 | lsb_release -d | grep --color=auto "Fedora" > /dev/null 37 | else 38 | uname -r | grep --color=auto "fc" > /dev/null 39 | fi 40 | ) 41 | end 42 | 43 | def self.arch_cmd 44 | %q( 45 | if $(which lsb_release >/dev/null 2>&1); then 46 | lsb_release -d | grep --color=auto "Arch" > /dev/null 47 | else 48 | uname -a | grep --color=auto "ARCH" > /dev/null 49 | fi 50 | ) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/dinghy_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::Dinghy do 2 | def dinghy_bin 3 | '/usr/local/bin/dinghy' 4 | end 5 | 6 | def stub_dinghy_ip(echo_results) 7 | dinghy_script = <<-EOF 8 | #!/usr/bin/env bash 9 | echo -e "#{echo_results}" 10 | EOF 11 | dinghy_script.gsub!(/^\s\s\s\s\s\s/, '') 12 | File.write('/tmp/dinghy', dinghy_script) 13 | Dory::Bash.run_command("sudo mv /tmp/dinghy #{dinghy_bin}") 14 | Dory::Bash.run_command("chmod +x #{dinghy_bin}") 15 | end 16 | 17 | def delete_dinghy_stub 18 | Dory::Bash.run_command("sudo rm #{dinghy_bin}") if File.exist?(dinghy_bin) 19 | end 20 | 21 | after :all do 22 | delete_dinghy_stub 23 | end 24 | 25 | it 'knows if dinghy is installed' do 26 | stub_dinghy_ip('5.5.5.5') 27 | expect(Dory::Dinghy.installed?).to be_truthy 28 | delete_dinghy_stub 29 | expect(Dory::Dinghy.installed?).to be_falsey 30 | end 31 | 32 | it 'gets the ip address of the dinghy vm' do 33 | stub_dinghy_ip('5.5.5.5') 34 | expect(Dory::Dinghy.ip).to eq('5.5.5.5') 35 | end 36 | 37 | it 'raises a DinghyError if dinghy doesnt provide an ip address' do 38 | stub_dinghy_ip('Some text') 39 | expect{ Dory::Dinghy.ip }.to raise_error(Dory::Dinghy::DinghyError) 40 | end 41 | 42 | it 'matches the dinghy string' do 43 | %w[ 44 | dinghy 45 | dingy 46 | dinhgy 47 | dinhy 48 | ].each do |str| 49 | expect(Dory::Dinghy.match?(str)).to be_truthy 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dory/systemd.rb: -------------------------------------------------------------------------------- 1 | require_relative 'docker_service' 2 | 3 | module Dory 4 | module Systemd 5 | def self.up_delay_seconds 6 | Dory::Config.settings[:dory][:dnsmasq][:service_start_delay] || 5 7 | end 8 | 9 | def self.has_systemd? 10 | Sh.run_command('which systemctl').success? 11 | end 12 | 13 | def self.systemd_service_installed?(service) 14 | return false unless self.has_systemd? 15 | !(Sh.run_command("systemctl status #{service} | head -1").stdout =~ /not-found/) 16 | end 17 | 18 | def self.systemd_service_running?(service) 19 | return false unless self.has_systemd? 20 | !!(Sh.run_command("systemctl status #{service} | head -3").stdout =~ /Active:\s+active.*running/) 21 | end 22 | 23 | def self.systemd_service_enabled?(service) 24 | return false unless self.has_systemd? 25 | !!(Sh.run_command("systemctl status #{service} | head -3").stdout.gsub(/Loaded.*?;/, '') =~ /^\s*enabled;/) 26 | end 27 | 28 | def self.set_systemd_service(service:, up:) 29 | action = up ? 'start' : 'stop' 30 | puts "Requesting sudo to #{action} #{service}".green 31 | retval = Sh.run_command("sudo systemctl #{action} #{service}").success? 32 | 33 | # We need to wait a few seconds for init if putting stuff up to avoid race conditions 34 | if up 35 | puts "Waiting #{self.up_delay_seconds} seconds for #{service} to start ...".green 36 | sleep(self.up_delay_seconds) 37 | end 38 | 39 | retval 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /dory.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'dory/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'dory' 8 | s.version = Dory.version 9 | s.date = Dory.date 10 | s.summary = 'Your development proxy for Docker' 11 | s.description = 'Dory lets you forget about IP addresses and ' \ 12 | 'port numbers while you are developing your application. ' \ 13 | 'Through the magic of local DNS and a reverse proxy, you ' \ 14 | 'can access your app at the domain of your choosing. For ' \ 15 | 'example, http://myapp.docker or http://this-is-a-really-long-name.but-its-cool-cause-i-like-it. ' \ 16 | 'Check it out on github at: https://github.com/FreedomBen/dory' 17 | s.authors = ['Ben Porter'] 18 | s.email = 'BenjaminPorter86@gmail.com' 19 | s.files = ['lib/dory.rb'] + Dir['lib/dory/**/*'] 20 | s.homepage = 'https://github.com/FreedomBen/dory' 21 | s.license = 'MIT' 22 | 23 | s.required_ruby_version = '>= 2.1.0' 24 | 25 | s.executables << 'dory' 26 | 27 | s.add_runtime_dependency 'colorize', '~> 0.8' 28 | s.add_runtime_dependency 'thor', '~> 1.2' 29 | s.add_runtime_dependency 'ptools', '~> 1.4' 30 | s.add_runtime_dependency 'activesupport', '>= 5.2', '< 8.0' 31 | 32 | s.add_development_dependency 'rspec', '~> 3.11' 33 | s.add_development_dependency 'rake', '~> 13.0' 34 | s.add_development_dependency 'byebug', '~> 11.1' 35 | s.add_development_dependency 'codeclimate-test-reporter', '~> 1.0' 36 | s.add_development_dependency 'rubocop', '~> 1.36' 37 | end 38 | -------------------------------------------------------------------------------- /spec/lib/upgrade_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | RSpec.describe Dory::Upgrade do 4 | context 'parsing the version' do 5 | let(:version_num) { "2.2.2" } 6 | 7 | let(:version_str) do 8 | ->(version_number) do 9 | "dory (#{version_number})\n" 10 | end 11 | end 12 | 13 | let(:stub_sh) do 14 | ->(success, stdout) do 15 | allow(Dory::Sh).to receive(:run_command) do 16 | OpenStruct.new(success?: success, stdout: stdout) 17 | end 18 | end 19 | end 20 | 21 | it "parses the new version" do 22 | stub_sh.call(true, version_str.call(version_num)) 23 | expect(Dory::Upgrade.new_version).to eq(version_num) 24 | end 25 | 26 | it "returns an error if the regex parses multiple matches" do 27 | stub_sh.call(true, 'dory some version') 28 | expect(Dory::Upgrade.new_version).to be_falsey 29 | end 30 | 31 | it "returns an error if the regex parses multiple matches" do 32 | stub_sh.call(false, '') 33 | expect(Dory::Upgrade.new_version).to be_falsey 34 | end 35 | end 36 | 37 | it 'knows if it is outdated' do 38 | expect(Dory::Upgrade.outdated?(Dory.version)).to be_falsey 39 | expect(Dory::Upgrade.outdated?('fake version')).to be_truthy 40 | end 41 | 42 | it 'gem installs a new dory' do 43 | allow(Dory::Sh).to receive(:run_command).with('gem install dory') { true } 44 | expect(Dory::Upgrade.install).to be_truthy 45 | end 46 | 47 | it 'cleans up old gems' do 48 | allow(Dory::Sh).to receive(:run_command).with('gem cleanup dory') { true } 49 | expect(Dory::Upgrade.cleanup).to be_truthy 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/dory/proxy.rb: -------------------------------------------------------------------------------- 1 | require_relative 'docker_service' 2 | 3 | module Dory 4 | class Proxy 5 | extend Dory::DockerService 6 | 7 | def self.dory_http_proxy_image_name 8 | setting = Dory::Config.settings[:dory][:nginx_proxy][:image] 9 | return setting if setting 10 | certs_dir && !certs_dir.empty? \ 11 | ? 'codekitchen/dinghy-http-proxy:2.5.10' \ 12 | : 'freedomben/dory-http-proxy:2.6.2.2' 13 | end 14 | 15 | def self.container_name 16 | Dory::Config.settings[:dory][:nginx_proxy][:container_name] 17 | end 18 | 19 | def self.certs_dir 20 | Dory::Config.settings[:dory][:nginx_proxy][:ssl_certs_dir] 21 | end 22 | 23 | def self.certs_arg 24 | if certs_dir && !certs_dir.empty? 25 | "-v #{certs_dir}:/etc/nginx/certs" 26 | else 27 | '' 28 | end 29 | end 30 | 31 | def self.tls_arg 32 | if [:tls_enabled, :ssl_enabled, :https_enabled].any? { |s| 33 | Dory::Config.settings[:dory][:nginx_proxy][s] 34 | } 35 | "-p #{Dory::Config.settings[:dory][:nginx_proxy][:tls_port]}:443" 36 | else 37 | '' 38 | end 39 | end 40 | 41 | def self.http_port 42 | Dory::Config.settings[:dory][:nginx_proxy][:port] 43 | end 44 | 45 | def self.run_command 46 | "docker run -d -p #{http_port}:80 #{self.tls_arg} #{self.certs_arg} "\ 47 | "-v /var/run/docker.sock:/tmp/docker.sock -e " \ 48 | "'CONTAINER_NAME=#{Shellwords.escape(self.container_name)}' --name " \ 49 | "'#{Shellwords.escape(self.container_name)}' " \ 50 | "#{Shellwords.escape(dory_http_proxy_image_name)}" 51 | end 52 | 53 | def self.start_cmd 54 | "docker start #{Shellwords.escape(self.container_name)}" 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | dory (1.2.0) 5 | activesupport (>= 5.2, < 8.0) 6 | colorize (~> 0.8) 7 | ptools (~> 1.4) 8 | thor (~> 1.2) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activesupport (7.0.4) 14 | concurrent-ruby (~> 1.0, >= 1.0.2) 15 | i18n (>= 1.6, < 2) 16 | minitest (>= 5.1) 17 | tzinfo (~> 2.0) 18 | ast (2.4.2) 19 | byebug (11.1.3) 20 | codeclimate-test-reporter (1.0.9) 21 | simplecov (<= 0.13) 22 | colorize (0.8.1) 23 | concurrent-ruby (1.1.10) 24 | diff-lcs (1.5.0) 25 | docile (1.1.5) 26 | i18n (1.12.0) 27 | concurrent-ruby (~> 1.0) 28 | json (2.6.2) 29 | minitest (5.16.3) 30 | parallel (1.22.1) 31 | parser (3.1.2.1) 32 | ast (~> 2.4.1) 33 | ptools (1.4.2) 34 | rainbow (3.1.1) 35 | rake (13.0.6) 36 | regexp_parser (2.5.0) 37 | rexml (3.2.5) 38 | rspec (3.11.0) 39 | rspec-core (~> 3.11.0) 40 | rspec-expectations (~> 3.11.0) 41 | rspec-mocks (~> 3.11.0) 42 | rspec-core (3.11.0) 43 | rspec-support (~> 3.11.0) 44 | rspec-expectations (3.11.1) 45 | diff-lcs (>= 1.2.0, < 2.0) 46 | rspec-support (~> 3.11.0) 47 | rspec-mocks (3.11.1) 48 | diff-lcs (>= 1.2.0, < 2.0) 49 | rspec-support (~> 3.11.0) 50 | rspec-support (3.11.1) 51 | rubocop (1.36.0) 52 | json (~> 2.3) 53 | parallel (~> 1.10) 54 | parser (>= 3.1.2.1) 55 | rainbow (>= 2.2.2, < 4.0) 56 | regexp_parser (>= 1.8, < 3.0) 57 | rexml (>= 3.2.5, < 4.0) 58 | rubocop-ast (>= 1.20.1, < 2.0) 59 | ruby-progressbar (~> 1.7) 60 | unicode-display_width (>= 1.4.0, < 3.0) 61 | rubocop-ast (1.21.0) 62 | parser (>= 3.1.1.0) 63 | ruby-progressbar (1.11.0) 64 | simplecov (0.13.0) 65 | docile (~> 1.1.0) 66 | json (>= 1.8, < 3) 67 | simplecov-html (~> 0.10.0) 68 | simplecov-html (0.10.2) 69 | thor (1.2.1) 70 | tzinfo (2.0.5) 71 | concurrent-ruby (~> 1.0) 72 | unicode-display_width (2.3.0) 73 | 74 | PLATFORMS 75 | x86_64-linux 76 | 77 | DEPENDENCIES 78 | byebug (~> 11.1) 79 | codeclimate-test-reporter (~> 1.0) 80 | dory! 81 | rake (~> 13.0) 82 | rspec (~> 3.11) 83 | rubocop (~> 1.36) 84 | 85 | BUNDLED WITH 86 | 2.3.8 87 | -------------------------------------------------------------------------------- /lib/dory/resolv/linux.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Dory 4 | module Resolv 5 | module Linux 6 | def self.common_resolv_file 7 | '/etc/resolv.conf' 8 | end 9 | 10 | def self.ubuntu_resolv_file 11 | #'/etc/resolvconf/resolv.conf.d/base' 12 | # For now, use the common_resolv_file 13 | self.common_resolv_file 14 | end 15 | 16 | def self.file_comment 17 | '# added by dory' 18 | end 19 | 20 | def self.nameserver 21 | Dory::Config.settings[:dory][:resolv][:nameserver] 22 | end 23 | 24 | # Note that we ignore any ports present in the config 25 | # file because only port 53 is supported on linux 26 | # (and there's not a way to specify it in the resolv.conf 27 | # even if we wanted to, which someday hopefully we can) 28 | def self.file_nameserver_line 29 | "nameserver #{self.nameserver}" 30 | end 31 | 32 | def self.nameserver_contents 33 | "#{self.file_nameserver_line} #{self.file_comment}" 34 | end 35 | 36 | def self.resolv_file 37 | if Os.ubuntu? 38 | return self.ubuntu_resolv_file if Os.ubuntu? 39 | elsif Os.fedora? || Os.arch? || File.exist?(self.common_resolv_file) 40 | return self.common_resolv_file 41 | else 42 | raise RuntimeError.new( 43 | "Unable to determine location of resolv file" 44 | ) 45 | end 46 | end 47 | 48 | def self.configure 49 | # we want to be the first nameserver in the list for performance reasons 50 | # we only want to add the nameserver if it isn't already there 51 | prev_conts = self.resolv_file_contents 52 | unless self.contents_has_our_nameserver?(prev_conts) 53 | if prev_conts =~ /nameserver/ 54 | prev_conts.sub!(/^\s*nameserver/, "#{self.nameserver_contents}\nnameserver") 55 | else 56 | prev_conts = "#{prev_conts}\n#{self.nameserver_contents}" 57 | end 58 | prev_conts.gsub!(/\s+$/, '') 59 | self.write_to_file(prev_conts) 60 | end 61 | self.has_our_nameserver? 62 | end 63 | 64 | def self.clean 65 | prev_conts = self.resolv_file_contents 66 | if self.contents_has_our_nameserver?(prev_conts) 67 | prev_conts.gsub!(/#{Regexp.escape(self.nameserver_contents + "\n")}/, '') 68 | prev_conts.gsub!(/\s+$/, '') 69 | self.write_to_file(prev_conts) 70 | end 71 | !self.has_our_nameserver? 72 | end 73 | 74 | def self.write_to_file(contents) 75 | # have to use this hack cuz we don't run as root :-( 76 | puts "Requesting sudo to write to #{self.resolv_file}".green 77 | Bash.run_command("echo -e '#{Bash.escape_single_quotes(contents)}' | sudo /usr/bin/tee #{Shellwords.escape(self.resolv_file)} >/dev/null") 78 | end 79 | 80 | def self.resolv_file_contents 81 | File.read(self.resolv_file) 82 | end 83 | 84 | def self.has_our_nameserver? 85 | self.contents_has_our_nameserver?(self.resolv_file_contents) 86 | end 87 | 88 | def self.contents_has_our_nameserver?(contents) 89 | !!((contents =~ /#{self.file_comment}/) && (contents =~ /#{self.file_nameserver_line}/)) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/dory/resolv/macos.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Dory 4 | module Resolv 5 | module Macos 6 | def self.system_resolv_file 7 | '/etc/resolv.conf' 8 | end 9 | 10 | def self.port 11 | Dory::Config.settings[:dory][:resolv][:port] || 19323 12 | end 13 | 14 | def self.resolv_dir 15 | '/etc/resolver' 16 | end 17 | 18 | def self.resolv_file_names 19 | # on macos the file name should match the domain 20 | if Dory::Config.settings[:dory][:dnsmasq][:domain] 21 | [Dory::Config.settings[:dory][:dnsmasq][:domain]] 22 | elsif Dory::Config.settings[:dory][:dnsmasq][:domains] 23 | Dory::Config.settings[:dory][:dnsmasq][:domains].map{ |d| d[:domain] } 24 | else 25 | ['docker'] 26 | end 27 | end 28 | 29 | def self.resolv_files 30 | self.resolv_file_names.map{ |f| "#{self.resolv_dir}/#{f}" } 31 | end 32 | 33 | def self.configured_to_use_dinghy 34 | Dory::Dinghy.match?(Dory::Config.settings[:dory][:resolv][:nameserver]) 35 | end 36 | 37 | def self.nameserver 38 | ns = Dory::Config.settings[:dory][:resolv][:nameserver] 39 | Dory::Dinghy.match?(ns) ? Dory::Dinghy.ip : ns 40 | end 41 | 42 | def self.file_nameserver_line 43 | "nameserver #{self.nameserver}" 44 | end 45 | 46 | def self.file_comment 47 | '# added by dory' 48 | end 49 | 50 | def self.resolv_contents 51 | <<-EOF.gsub(' ' * 10, '') 52 | #{self.file_comment} 53 | #{self.file_nameserver_line} 54 | port #{self.port} 55 | EOF 56 | end 57 | 58 | def self.configure 59 | # have to use this hack cuz we don't run as root :-( 60 | unless Dir.exist?(self.resolv_dir) 61 | puts "Requesting sudo to create directory #{self.resolv_dir}".green 62 | Bash.run_command("sudo mkdir -p #{self.resolv_dir}") 63 | end 64 | self.resolv_files.each do |filename| 65 | puts "Requesting sudo to write to #{filename}".green 66 | Bash.run_command("echo -e '#{self.resolv_contents}' | sudo /usr/bin/tee #{Shellwords.escape(filename)} >/dev/null") 67 | end 68 | rescue DinghyError => e 69 | puts e.message.red 70 | false 71 | end 72 | 73 | def self.clean 74 | self.resolv_files.each do |filename| 75 | puts "Requesting sudo to delete '#{filename}'".green 76 | Bash.run_command("sudo rm -f #{filename}") 77 | end 78 | end 79 | 80 | def self.system_resolv_file_contents 81 | File.read(self.system_resolv_file) 82 | end 83 | 84 | def self.resolv_file_contents 85 | File.read(self.resolv_file) 86 | end 87 | 88 | def self.has_our_nameserver? 89 | self.resolv_files.all? do |filename| 90 | if File.exist?(filename) 91 | self.contents_has_our_nameserver?(File.read(filename)) 92 | else 93 | false 94 | end 95 | end 96 | end 97 | 98 | def self.contents_has_our_nameserver?(contents) 99 | comment_match = contents =~ /#{self.file_comment}/ 100 | port_match = contents =~ /port.#{self.port}/ 101 | if configured_to_use_dinghy 102 | !!(comment_match && port_match) 103 | else 104 | nameserver_match = contents =~ /#{self.file_nameserver_line}/ 105 | !!(comment_match && port_match && nameserver_match) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/dory/docker_service.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | require 'ostruct' 3 | 4 | module Dory 5 | module DockerService 6 | def self.docker_installed? 7 | Sh.run_command('which docker').success? 8 | end 9 | 10 | def docker_installed? 11 | Dory::DockerService.docker_installed? 12 | end 13 | 14 | def run_preconditions 15 | # Override if preconditions are needed 16 | return true 17 | end 18 | 19 | def run_postconditions 20 | # Override if postconditions are needed 21 | return true 22 | end 23 | 24 | def handle_error(_command_output) 25 | # Override to provide error handling 26 | return false 27 | end 28 | 29 | def start(handle_error: true) 30 | if self.running? 31 | if Dory::Config.debug? 32 | puts "[DEBUG] Container '#{self.container_name}' is already running. Doing nothing" 33 | end 34 | else 35 | if docker_installed? 36 | self.delete_container_if_exists 37 | self.run_preconditions 38 | self.execute_run_command(handle_error: handle_error) 39 | self.run_postconditions 40 | else 41 | err_msg = "Docker does not appear to be installed /o\\\n" \ 42 | "Docker is required for DNS and Nginx proxy. These can be " \ 43 | "disabled in the config file if you don't need them." 44 | puts err_msg.red 45 | end 46 | end 47 | self.running? 48 | end 49 | 50 | def running?(container_name = self.container_name) 51 | return false unless docker_installed? 52 | !!(self.ps =~ /#{container_name}/) 53 | end 54 | 55 | def container_exists?(container_name = self.container_name) 56 | !!(self.ps(all: true) =~ /#{container_name}/) 57 | end 58 | 59 | def ps(all: false) 60 | cmd = "docker ps#{all ? ' -a' : ''}" 61 | ret = Sh.run_command(cmd) 62 | if ret.success? 63 | return ret.stdout 64 | else 65 | raise RuntimeError.new("Failure running command '#{cmd}'") 66 | end 67 | end 68 | 69 | def stop(container_name = self.container_name) 70 | Sh.run_command("docker kill #{Shellwords.escape(container_name)}") if self.running? 71 | !self.running? 72 | end 73 | 74 | def delete(container_name = self.container_name) 75 | if self.container_exists? 76 | self.stop if self.running? 77 | Sh.run_command("docker rm #{Shellwords.escape(container_name)}") 78 | end 79 | !self.container_exists? 80 | end 81 | 82 | def start_cmd 83 | "docker start #{Shellwords.escape(self.container_name)}" 84 | end 85 | 86 | def delete_container_if_exists 87 | if self.container_exists? 88 | puts "[DEBUG] Container '#{self.container_name}' exists. Deleting" if Dory::Config.debug? 89 | self.delete 90 | end 91 | end 92 | 93 | def execute_run_command(handle_error:) 94 | begin 95 | if Dory::Config.debug? 96 | puts "[DEBUG] '#{self.container_name}' does not exist. Creating/starting " \ 97 | "'#{self.container_name}' with '#{self.run_command}'" 98 | end 99 | status = Sh.run_command(self.run_command) 100 | unless status.success? 101 | if !handle_error || !self.handle_error(status) 102 | puts "Failed to start docker container '#{self.container_name}' " \ 103 | ". Command '#{self.run_command}' failed".red 104 | end 105 | end 106 | rescue Dory::Dinghy::DinghyError => e 107 | puts e.message.red 108 | end 109 | status 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | #require 'codeclimate-test-reporter' 2 | #CodeClimate::TestReporter.start 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'dory' 6 | 7 | # This file was generated by the `rspec --init` command. Conventionally, all 8 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 9 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 10 | # file to always be loaded, without a need to explicitly require it in any files. 11 | # 12 | # Given that it is always loaded, you are encouraged to keep this file as 13 | # light-weight as possible. Requiring heavyweight dependencies from this file 14 | # will add to the boot time of your test suite on EVERY test run, even for an 15 | # individual file that may not need all of that loaded. Instead, consider making 16 | # a separate helper file that requires the additional dependencies and performs 17 | # the additional setup, and require it from the spec files that actually need it. 18 | # 19 | # The `.rspec` file also contains a few flags that are not defaults but that 20 | # users commonly want. 21 | # 22 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 23 | RSpec.configure do |config| 24 | # rspec-expectations config goes here. You can use an alternate 25 | # assertion/expectation library such as wrong or the stdlib/minitest 26 | # assertions if you prefer. 27 | config.expect_with :rspec do |expectations| 28 | # This option will default to `true` in RSpec 4. It makes the `description` 29 | # and `failure_message` of custom matchers include text for helper methods 30 | # defined using `chain`, e.g.: 31 | # be_bigger_than(2).and_smaller_than(4).description 32 | # # => "be bigger than 2 and smaller than 4" 33 | # ...rather than: 34 | # # => "be bigger than 2" 35 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 36 | end 37 | 38 | # rspec-mocks config goes here. You can use an alternate test double 39 | # library (such as bogus or mocha) by changing the `mock_with` option here. 40 | config.mock_with :rspec do |mocks| 41 | # Prevents you from mocking or stubbing a method that does not exist on 42 | # a real object. This is generally recommended, and will default to 43 | # `true` in RSpec 4. 44 | mocks.verify_partial_doubles = true 45 | end 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # These two settings work together to allow you to limit a spec run 51 | # to individual examples or groups you care about by tagging them with 52 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 53 | # get run. 54 | config.filter_run :focus 55 | config.run_all_when_everything_filtered = true 56 | 57 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 58 | # For more details, see: 59 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 60 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 61 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 62 | config.disable_monkey_patching! 63 | 64 | # This setting enables warnings. It's recommended, but in some cases may 65 | # be too noisy due to issues in dependencies. 66 | config.warnings = true 67 | 68 | # Many RSpec users commonly either run the entire suite or an individual 69 | # file, and it's useful to allow more verbose output when running an 70 | # individual spec file. 71 | if config.files_to_run.one? 72 | # Use the documentation formatter for detailed output, 73 | # unless a formatter has already been configured 74 | # (e.g. via a command-line flag). 75 | config.default_formatter = 'doc' 76 | end 77 | 78 | # Print the 10 slowest examples and example groups at the 79 | # end of the spec run, to help surface which specs are running 80 | # particularly slow. 81 | config.profile_examples = 10 82 | 83 | # Run specs in random order to surface order dependencies. If you find an 84 | # order dependency and want to debug it, you can fix the order by providing 85 | # the seed, which is printed after each run. 86 | # --seed 1234 87 | config.order = :random 88 | 89 | # Seed global randomization in this process using the `--seed` CLI option. 90 | # Setting this allows you to use `--seed` to deterministically reproduce 91 | # test failures related to randomization by passing the same `--seed` value 92 | # as the one that triggered the failure. 93 | Kernel.srand config.seed 94 | =end 95 | end 96 | -------------------------------------------------------------------------------- /lib/dory/config.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'active_support/core_ext/hash/indifferent_access' 3 | 4 | module Dory 5 | class Config 6 | def self.has_config_file?(dir) 7 | File.exist?("#{dir}/.dory.yml") 8 | end 9 | 10 | def self.find_config_file(starting_dir) 11 | if self.has_config_file?(starting_dir) 12 | "#{starting_dir}/.dory.yml" 13 | elsif starting_dir == "/" 14 | return self.default_filename 15 | else 16 | return self.find_config_file(File.dirname(starting_dir)) # recurse up to / 17 | end 18 | end 19 | 20 | def self.default_filename 21 | "#{Dir.home}/.dory.yml" 22 | end 23 | 24 | def self.filename 25 | self.find_config_file(Dir.pwd) 26 | end 27 | 28 | def self.default_yaml 29 | %q(--- 30 | dory: 31 | # Be careful if you change the settings of some of 32 | # these services. They may not talk to each other 33 | # if you change IP Addresses. 34 | # For example, resolv expects a nameserver listening at 35 | # the specified address. dnsmasq normally does this, 36 | # but if you disable dnsmasq, it 37 | # will make your system look for a name server that 38 | # doesn't exist. 39 | dnsmasq: 40 | enabled: true 41 | domains: # array of domains that will be resolved to the specified address 42 | - domain: docker # you can set '#' for a wildcard 43 | address: 127.0.0.1 # return for queries against the domain 44 | container_name: dory_dnsmasq 45 | port: 53 # port to listen for dns requests on. must be 53 on linux. can be anything that's open on macos 46 | # kill_others: kill processes bound to the port we need (see previous setting 'port') 47 | # Possible values: 48 | # ask (prompt about killing each time. User can accept/reject) 49 | # yes|true (go aheand and kill without asking) 50 | # no|false (don't kill, and don't even ask) 51 | kill_others: ask 52 | service_start_delay: 5 # seconds to wait after restarting systemd services 53 | nginx_proxy: 54 | enabled: true 55 | container_name: dory_dinghy_http_proxy 56 | https_enabled: true 57 | ssl_certs_dir: '' # leave as empty string to use default certs 58 | port: 80 # port 80 is default for http 59 | tls_port: 443 # port 443 is default for https 60 | resolv: 61 | enabled: true 62 | nameserver: 127.0.0.1 63 | port: 53 # port where the nameserver listens. On linux it must be 53 64 | ).split("\n").map{|s| s.sub(' ' * 8, '')}.join("\n") 65 | end 66 | 67 | def self.default_settings 68 | YAML.load(self.default_yaml).with_indifferent_access 69 | end 70 | 71 | def self.settings(filename = self.filename) 72 | if File.exist?(filename) 73 | defaults = self.default_settings.dup 74 | config_file_settings = YAML.load_file(filename).with_indifferent_access 75 | [:dnsmasq, :nginx_proxy, :resolv].each do |service| 76 | defaults[:dory][service].merge!(config_file_settings[:dory][service] || {}) 77 | end 78 | defaults[:dory][:debug] = config_file_settings[:dory][:debug] 79 | defaults 80 | else 81 | self.default_settings 82 | end 83 | end 84 | 85 | def self.write_settings(settings, filename = self.filename, is_yaml: false) 86 | settings = settings.to_yaml unless is_yaml 87 | settings.gsub!(/\s*!ruby\/hash:ActiveSupport::HashWithIndifferentAccess/, '') 88 | File.write(filename, settings) 89 | end 90 | 91 | def self.write_default_settings_file(filename = self.filename) 92 | self.write_settings(self.default_yaml, filename, is_yaml: true) 93 | end 94 | 95 | def self.upgrade_settings_file(filename = self.filename) 96 | self.write_settings(self.upgrade(self.settings), filename, is_yaml: false) 97 | end 98 | 99 | def self.debug? 100 | self.settings[:dory][:debug] 101 | end 102 | 103 | def self.upgrade(old_hash) 104 | newsettings = old_hash.dup 105 | 106 | # If there's a single domain and address, upgrade to the array format 107 | if newsettings[:dory][:dnsmasq][:domain] 108 | newsettings[:dory][:dnsmasq][:domains] = [{ 109 | domain: newsettings[:dory][:dnsmasq][:domain], 110 | address: newsettings[:dory][:dnsmasq][:address] || '127.0.0.1' 111 | }] 112 | newsettings[:dory][:dnsmasq].delete(:domain) 113 | newsettings[:dory][:dnsmasq].delete(:address) 114 | end 115 | 116 | # Add the option to skip prompts 117 | unless newsettings[:dory][:dnsmasq][:kill_others] 118 | newsettings[:dory][:dnsmasq][:kill_others] = 'ask' 119 | end 120 | 121 | unless newsettings[:dory][:dnsmasq][:service_start_delay] 122 | newsettings[:dory][:dnsmasq][:service_start_delay] = 5 123 | end 124 | 125 | # add settings for nginx proxy port 126 | unless newsettings[:dory][:nginx_proxy][:port] 127 | newsettings[:dory][:nginx_proxy][:port] = 80 128 | end 129 | unless newsettings[:dory][:nginx_proxy][:tls_port] 130 | newsettings[:dory][:nginx_proxy][:tls_port] = 443 131 | end 132 | 133 | newsettings 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/lib/proxy_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::Proxy do 2 | after :all do 3 | Dory::Proxy.delete 4 | end 5 | 6 | it "has the docker client" do 7 | expect(Dory::Proxy.docker_installed?).to be_truthy 8 | end 9 | 10 | it "starts up the container" do 11 | Dory::Proxy.delete 12 | expect(Dory::Proxy).not_to be_running 13 | expect{Dory::Proxy.start}.to change{Dory::Proxy.running?}.from(false).to(true) 14 | expect(Dory::Proxy).to be_container_exists 15 | end 16 | 17 | it "doesn't fail when starting the container twice" do 18 | 2.times{ expect{Dory::Proxy.start}.not_to raise_error } 19 | expect(Dory::Proxy).to be_running 20 | end 21 | 22 | it "deletes the container" do 23 | expect(Dory::Proxy.start).to be_truthy 24 | expect(Dory::Proxy).to be_running 25 | expect(Dory::Proxy).to be_container_exists 26 | expect{Dory::Proxy.delete}.to change{Dory::Proxy.container_exists?}.from(true).to(false) 27 | expect(Dory::Proxy).not_to be_running 28 | end 29 | 30 | it "stops the container" do 31 | expect(Dory::Proxy.start).to be_truthy 32 | expect(Dory::Proxy).to be_running 33 | expect(Dory::Proxy.stop).to be_truthy 34 | expect(Dory::Proxy).to be_container_exists 35 | expect(Dory::Proxy).not_to be_running 36 | end 37 | 38 | it "starts the container when it already exists" do 39 | expect(Dory::Proxy.start).to be_truthy 40 | expect(Dory::Proxy).to be_running 41 | expect(Dory::Proxy.stop).to be_truthy 42 | expect(Dory::Proxy).to be_container_exists 43 | expect{Dory::Proxy.start}.to change{Dory::Proxy.running?}.from(false).to(true) 44 | expect(Dory::Proxy).to be_container_exists 45 | expect(Dory::Proxy).to be_running 46 | end 47 | 48 | context "ssl" do 49 | let(:filename) { '/tmp/dory-test-config-yml' } 50 | 51 | let(:expect_proxy_to_start) do 52 | ->() do 53 | Dory::Proxy.delete 54 | expect(Dory::Proxy).not_to be_running 55 | expect{Dory::Proxy.start}.to change{Dory::Proxy.running?}.from(false).to(true) 56 | expect(Dory::Proxy).to be_container_exists 57 | end 58 | end 59 | 60 | let(:patch_ssl_enabled) do 61 | ->(enabled) do 62 | new_config = YAML.load(Dory::Config.default_yaml) 63 | new_config['dory']['nginx_proxy']['https_enabled'] = enabled 64 | expect(new_config['dory']['nginx_proxy']['https_enabled']).to eq(enabled) 65 | allow(Dory::Config).to receive(:default_yaml) { new_config.to_yaml } 66 | expect(Dory::Config.settings['dory']['nginx_proxy']['https_enabled']).to eq(enabled) 67 | end 68 | end 69 | 70 | let(:expect_port) do 71 | ->(in_cmd:) do 72 | if in_cmd 73 | expect(Dory::Proxy.run_command).to match(/-p\s+443:443/) 74 | else 75 | expect(Dory::Proxy.run_command).not_to match(/-p\s+443:443/) 76 | end 77 | end 78 | end 79 | 80 | before :each do 81 | allow(Dory::Config).to receive(:filename) { filename } 82 | end 83 | 84 | context "enabled" do 85 | it "enables" do 86 | patch_ssl_enabled.call(true) 87 | expect_port.call(in_cmd: true) 88 | expect_proxy_to_start.call() 89 | end 90 | 91 | it "disables" do 92 | patch_ssl_enabled.call(false) 93 | expect_port.call(in_cmd: false) 94 | expect_proxy_to_start.call() 95 | end 96 | end 97 | 98 | context "port" do 99 | let(:new_port) { 1234 } 100 | let(:new_tls_port) { 4567 } 101 | 102 | let(:patch_ports) do 103 | ->(port:, tls_port:) do 104 | new_config = YAML.load(Dory::Config.default_yaml) 105 | new_config['dory']['nginx_proxy']['port'] = port 106 | new_config['dory']['nginx_proxy']['tls_port'] = tls_port 107 | expect(new_config['dory']['nginx_proxy']['port']).to eq(port) 108 | expect(new_config['dory']['nginx_proxy']['tls_port']).to eq(tls_port) 109 | allow(Dory::Config).to receive(:default_yaml) { new_config.to_yaml } 110 | expect(Dory::Config.settings['dory']['nginx_proxy']['port']).to eq(port) 111 | expect(Dory::Config.settings['dory']['nginx_proxy']['tls_port']).to eq(tls_port) 112 | end 113 | end 114 | 115 | it "defaults to 80 for http" do 116 | dy = YAML.load(Dory::Config.default_yaml) 117 | expect(dy['dory']['nginx_proxy']['port']).to eq(80) 118 | expect(dy['dory']['nginx_proxy']['tls_port']).to eq(443) 119 | expect(Dory::Proxy.run_command).to match(/-p\s+80:80/) 120 | expect(Dory::Proxy.run_command).to match(/-p\s+443:443/) 121 | end 122 | 123 | it "allows changing the ports" do 124 | dy = YAML.load(Dory::Config.default_yaml) 125 | expect(dy['dory']['nginx_proxy']['port']).to eq(80) 126 | expect(dy['dory']['nginx_proxy']['tls_port']).to eq(443) 127 | expect(Dory::Proxy.run_command).to match(/-p\s+80:80/) 128 | expect(Dory::Proxy.run_command).to match(/-p\s+443:443/) 129 | 130 | patch_ports.call(port: new_port, tls_port: new_tls_port) 131 | dy = YAML.load(Dory::Config.default_yaml) 132 | expect(dy['dory']['nginx_proxy']['port']).to eq(new_port) 133 | expect(dy['dory']['nginx_proxy']['tls_port']).to eq(new_tls_port) 134 | require 'byebug'; debugger 135 | patch_ssl_enabled.call(true) 136 | expect(Dory::Proxy.run_command).to match(/-p\s+#{new_port}:80/) 137 | expect(Dory::Proxy.run_command).to match(/-p\s+#{new_tls_port}:443/) 138 | end 139 | end 140 | 141 | context "certs" do 142 | let(:patch_config_ssl_certs_dir) do 143 | ->(ssl_certs_dir) do 144 | new_config = YAML.load(Dory::Config.default_yaml) 145 | new_config['dory']['nginx_proxy']['ssl_certs_dir'] = ssl_certs_dir 146 | expect(new_config['dory']['nginx_proxy']['ssl_certs_dir']).to eq(ssl_certs_dir) 147 | allow(Dory::Config).to receive(:default_yaml) { new_config.to_yaml } 148 | expect(Dory::Config.settings[:dory][:nginx_proxy][:ssl_certs_dir]).to eq(ssl_certs_dir) 149 | end 150 | end 151 | 152 | let(:expect_not_in_cmd) do 153 | ->(ssl_certs_dir) do 154 | patch_config_ssl_certs_dir.call(ssl_certs_dir) 155 | expect(Dory::Proxy.run_command).not_to match(/-v\s.*..etc.nginx.certs/) 156 | expect_proxy_to_start.call() 157 | end 158 | end 159 | 160 | it "mounts the ssl certs dir when it's set" do 161 | ssl_certs_dir = '/usr/bin' 162 | patch_config_ssl_certs_dir.call(ssl_certs_dir) 163 | expect(Dory::Proxy.run_command).to match(/-v\s#{Regexp.escape(ssl_certs_dir)}..etc.nginx.certs/) 164 | expect_port.call(in_cmd: true) 165 | expect_proxy_to_start.call() 166 | end 167 | 168 | it "does not mount anything when ssl certs is empty string" do 169 | expect_not_in_cmd.call('') 170 | expect_proxy_to_start.call() 171 | end 172 | 173 | it "does not mount anything when ssl certs is nil" do 174 | expect_not_in_cmd.call(nil) 175 | expect_proxy_to_start.call() 176 | end 177 | 178 | it "uses the freedomben proxy image when no ssl certs are being mounted" do 179 | patch_config_ssl_certs_dir.call('') 180 | expect(Dory::Proxy.dory_http_proxy_image_name).to match(/^freedomben/) 181 | patch_config_ssl_certs_dir.call(nil) 182 | expect(Dory::Proxy.dory_http_proxy_image_name).to match(/^freedomben/) 183 | end 184 | 185 | it "uses the codekitchen proxy image when ssl certs are being mounted" do 186 | patch_config_ssl_certs_dir.call('/usr/bin') 187 | expect(Dory::Proxy.dory_http_proxy_image_name).to match(/^codekitchen/) 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /spec/lib/resolv/linux_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | RSpec.describe Dory::Resolv::Linux do 4 | let(:resolv_file) { '/tmp/resolve' } 5 | 6 | let(:ubuntu_resolv_file_contents) do 7 | %q( 8 | # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) 9 | # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN 10 | nameserver 10.0.34.17 11 | nameserver 10.0.34.16 12 | nameserver 10.0.201.16 13 | search corp.instructure.com 14 | ).split("\n").map{|s| s.gsub(/^\s+/, '')}.join("\n") 15 | end 16 | 17 | let(:fedora_resolv_file_contents) do 18 | %q( 19 | #@VPNC_GENERATED@ -- this file is generated by vpnc 20 | # and will be overwritten by vpnc 21 | # as long as the above mark is intact 22 | # Generated by NetworkManager 23 | search lan corp.instructure.com 24 | nameserver 10.0.34.17 25 | ).split("\n").map{|s| s.gsub(/^\s+/, '')}.join("\n") 26 | end 27 | 28 | let(:fedora_alt_resolv_file_contents) do 29 | %q( 30 | # Generated by NetworkManager 31 | search lan 32 | nameserver 192.168.11.1 33 | ).split("\n").map{|s| s.gsub(/^\s+/, '')}.join("\n") 34 | end 35 | 36 | let(:stub_resolv_file) do 37 | ->(filename = resolv_file) do 38 | allow(Dory::Resolv::Linux).to receive(:common_resolv_file) { filename } 39 | allow(Dory::Resolv::Linux).to receive(:ubuntu_resolv_file) { filename } 40 | # make sure we aren't going to over-write the real resolv file 41 | expect(Dory::Resolv::Linux.resolv_file).to eq(filename) 42 | end 43 | end 44 | 45 | let(:set_ubuntu) do 46 | ->() do 47 | allow(Dory::Os).to receive(:ubuntu?){ true } 48 | allow(Dory::Os).to receive(:fedora?){ false } 49 | allow(Dory::Os).to receive(:arch?){ false } 50 | end 51 | end 52 | 53 | let(:set_fedora) do 54 | ->() do 55 | allow(Dory::Os).to receive(:ubuntu?){ false } 56 | allow(Dory::Os).to receive(:fedora?){ true } 57 | allow(Dory::Os).to receive(:arch?){ false } 58 | end 59 | end 60 | 61 | let(:set_arch) do 62 | ->() do 63 | allow(Dory::Os).to receive(:ubuntu?){ false } 64 | allow(Dory::Os).to receive(:fedora?){ false } 65 | allow(Dory::Os).to receive(:arch?){ true } 66 | end 67 | end 68 | 69 | let(:set_unknown_platform) do 70 | ->() do 71 | allow(Dory::Os).to receive(:ubuntu?){ false } 72 | allow(Dory::Os).to receive(:fedora?){ false } 73 | allow(Dory::Os).to receive(:arch?){ false } 74 | end 75 | end 76 | 77 | it 'returns common resolv file on fedora and arch' do 78 | stub_resolv_file.call() 79 | expect(Dory::Resolv::Linux.common_resolv_file).to eq(resolv_file) 80 | set_fedora.call() 81 | expect(Dory::Resolv::Linux.resolv_file).to eq(resolv_file) 82 | set_arch.call() 83 | expect(Dory::Resolv::Linux.resolv_file).to eq(resolv_file) 84 | end 85 | 86 | context "resolv file creation/destruction" do 87 | let(:filename) { '/tmp/thisfiledefinitelydoesnotexist.noexist' } 88 | 89 | before :each do 90 | stub_resolv_file.call(filename) 91 | expect(Dory::Resolv::Linux.common_resolv_file).to eq(filename) 92 | system("rm #{filename}") 93 | expect(File.exist?(Dory::Resolv::Linux.common_resolv_file)).to be_falsey 94 | end 95 | 96 | it 'returns common resolv file on unknown platform if it exists' do 97 | expect{ 98 | system("touch #{filename}") 99 | }.to change{File.exist?(Dory::Resolv::Linux.common_resolv_file)}.from(false).to(true) 100 | set_unknown_platform.call() 101 | expect(Dory::Resolv::Linux.resolv_file).to eq(filename) 102 | expect{ 103 | system("rm #{filename}") 104 | }.to change{File.exist?(Dory::Resolv::Linux.common_resolv_file)}.from(true).to(false) 105 | expect{Dory::Resolv::Linux.resolv_file}.to raise_error(RuntimeError, /unable.*location.*resolv.*file/i) 106 | end 107 | end 108 | 109 | context "editing the file" do 110 | let(:file_contents) do 111 | [ 112 | ubuntu_resolv_file_contents, 113 | fedora_resolv_file_contents, 114 | fedora_alt_resolv_file_contents, 115 | "# some comments\n # more comments\n", 116 | "\n", # empty file 117 | ] 118 | end 119 | 120 | before :each do 121 | stub_resolv_file.call() 122 | # To add an extra layer of protection against modifying the 123 | # real resolv file, make sure it matches 124 | expect(Dory::Resolv::Linux.resolv_file).to eq(resolv_file) 125 | end 126 | 127 | it "adds the nameserver when it doesn't exist" do 128 | file_contents.each do |contents| 129 | File.write(resolv_file, contents) 130 | expect(Dory::Resolv::Linux).not_to have_our_nameserver 131 | expect{Dory::Resolv::Linux.configure}.to change{Dory::Resolv::Linux.has_our_nameserver?}.from(false).to(true) 132 | expect(Dory::Resolv::Linux).to have_our_nameserver 133 | end 134 | end 135 | 136 | it "doesn't add the nameserver twice" do 137 | file_contents.each do |contents| 138 | File.write(resolv_file, contents) 139 | expect(Dory::Resolv::Linux).not_to have_our_nameserver 140 | expect{Dory::Resolv::Linux.configure}.to change{Dory::Resolv::Linux.has_our_nameserver?}.from(false).to(true) 141 | expect(Dory::Resolv::Linux).to have_our_nameserver 142 | lines = Dory::Resolv::Linux.resolv_file_contents.split("\n") 143 | nameserver_found = false 144 | lines.each do |line| 145 | if nameserver_found 146 | expect(line).not_to match(/dory/) 147 | end 148 | nameserver_found = true if line =~ /dory/ 149 | end 150 | expect(nameserver_found).to be_truthy 151 | end 152 | end 153 | 154 | it "cleans up properly" do 155 | file_contents.each do |contents| 156 | File.write(resolv_file, contents) 157 | expect(Dory::Resolv::Linux).not_to have_our_nameserver 158 | expect{Dory::Resolv::Linux.configure}.to change{Dory::Resolv::Linux.has_our_nameserver?}.from(false).to(true) 159 | expect(Dory::Resolv::Linux).to have_our_nameserver 160 | expect{Dory::Resolv::Linux.clean}.to change{Dory::Resolv::Linux.has_our_nameserver?}.from(true).to(false) 161 | expect(Dory::Resolv::Linux).not_to have_our_nameserver 162 | expect(File.read(resolv_file)).to eq(contents) 163 | expect(Dory::Resolv::Linux.resolv_file_contents).to eq(contents) 164 | end 165 | end 166 | end 167 | 168 | context "knows if we've edited the file" do 169 | let(:comment) { '# added by dory' } 170 | 171 | let(:stub_resolv) do 172 | ->(nameserver, file_comment = comment) do 173 | allow(Dory::Resolv::Linux).to receive(:nameserver){ nameserver } 174 | allow(Dory::Resolv::Linux).to receive(:file_comment){ file_comment } 175 | expect(Dory::Resolv::Linux.nameserver).to eq(nameserver) 176 | expect(Dory::Resolv::Linux.file_comment).to eq(file_comment) 177 | end 178 | end 179 | 180 | %w[127.0.0.1 192.168.53.164].each do |nameserver| 181 | # Note: This addresses a bug encountered on Fedora 23 Cloud 182 | it "doesn't think we edited the file if 127.0.0.1 is there but the comment isn't" do 183 | stub_resolv.call(nameserver) 184 | contents = "nameserver 1.1.1.1\nnameserver #{nameserver}\nnameserver 2.2.2.2" 185 | expect(Dory::Resolv::Linux.contents_has_our_nameserver?(contents)).to be_falsey 186 | end 187 | 188 | it "does think we edited the file if 127.0.0.1 is there and the comment is also" do 189 | stub_resolv.call(nameserver) 190 | contents = "nameserver 1.1.1.1\n#{comment}\nnameserver #{nameserver}\nnameserver 2.2.2.2" 191 | expect(Dory::Resolv::Linux.contents_has_our_nameserver?(contents)).to be_truthy 192 | end 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /spec/lib/resolv/macos_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | RSpec.describe Dory::Resolv::Macos do 4 | let(:resolv_dir) { '/tmp/resolver' } 5 | let(:resolv_files) { %w[docker dory] } 6 | let(:system_resolv_file) { '/tmp/resolv.conf' } 7 | let(:filenames) { %w[/tmp/resolver/docker /tmp/resolver/dev /tmp/resolver/dory] } 8 | 9 | let(:stub_resolv_files) do 10 | ->(files = filenames) { allow(Dory::Resolv::Macos).to receive(:resolv_files) { files } } 11 | end 12 | 13 | let(:unstub_resolv_files) do 14 | ->() { allow(Dory::Resolv::Macos).to receive(:resolv_files).and_call_original } 15 | end 16 | 17 | let!(:set_macos) do 18 | allow(Dory::Os).to receive(:macos?){ true } 19 | allow(Dory::Os).to receive(:ubuntu?){ false } 20 | allow(Dory::Os).to receive(:fedora?){ false } 21 | allow(Dory::Os).to receive(:arch?){ false } 22 | end 23 | 24 | before :each do 25 | allow(Dory::Resolv::Macos).to receive(:resolv_dir) { resolv_dir } 26 | end 27 | 28 | after :each do 29 | allow(Dory::Resolv::Macos).to receive(:resolv_dir).and_call_original 30 | end 31 | 32 | context 'settings' do 33 | context 'resolv' do 34 | let(:default_port) { 19323 } 35 | let(:specified_port) { 9999 } 36 | let(:explicit_port) {{ dory: { resolv: { port: specified_port }}}} 37 | let(:implicit_port) {{ dory: { resolv: {}}}} 38 | let(:explicit_ip) {{ dory: { resolv: { nameserver: '5.5.5.5' }}}} 39 | let(:dinghy_ip) {{ dory: { resolv: { nameserver: 'dinghy' }}}} 40 | 41 | it 'has a default port if one is not specified' do 42 | allow(Dory::Config).to receive(:settings) { explicit_port } 43 | expect(Dory::Resolv::Macos.port).to eq(specified_port) 44 | end 45 | 46 | it "let's you specify a port" do 47 | allow(Dory::Config).to receive(:settings) { implicit_port } 48 | expect(Dory::Resolv::Macos.port).to eq(default_port) 49 | end 50 | 51 | it 'uses the specified nameserver' do 52 | allow(Dory::Config).to receive(:settings) { explicit_ip } 53 | expect(Dory::Resolv::Macos.nameserver).to eq('5.5.5.5') 54 | end 55 | 56 | it 'pulls the setting from dinghy if dinghy is specified' do 57 | allow(Dory::Dinghy).to receive(:ip) { '1.1.1.1' } 58 | allow(Dory::Config).to receive(:settings) { dinghy_ip } 59 | expect(Dory::Resolv::Macos.nameserver).to eq('1.1.1.1') 60 | end 61 | 62 | it 'returns false from #configure if dinghy doesnt return an IP address' do 63 | allow(Dory::Bash).to receive(:run_command) do 64 | OpenStruct.new({ stdout: 'Invalid operation' }) 65 | end 66 | allow(Dory::Config).to receive(:settings) { dinghy_ip } 67 | expect(Dory::Resolv.configure).to be_falsey 68 | end 69 | end 70 | 71 | context 'dnsmasq' do 72 | let(:domains) { %w[docker dev dory somethingelse] } 73 | let(:domains_settings) {{ dory: { dnsmasq: { 74 | domains: domains.map{|d| { domain: d, address: '127.0.0.1' } } 75 | }}}} 76 | 77 | it 'has a filename for each domain' do 78 | allow(Dory::Config).to receive(:settings) { domains_settings } 79 | expect(Dory::Resolv::Macos.resolv_file_names).to match_array(domains) 80 | expect(Dory::Resolv::Macos.resolv_files).to match_array( 81 | domains.map{|d| "#{Dory::Resolv::Macos.resolv_dir}/#{d}" } 82 | ) 83 | end 84 | end 85 | end 86 | 87 | context "creating and deleting the file" do 88 | before :each do 89 | stub_resolv_files.call 90 | # To add an extra layer of protection against modifying the 91 | # real resolv file, make sure it matches 92 | expect(Dory::Resolv::Macos.resolv_files).to match_array(filenames) 93 | filenames.each do |filename| 94 | if File.exist?(filename) 95 | puts "Requesting sudo to delete #{filename}".green 96 | Dory::Bash.run_command("sudo rm -f #{filename}") 97 | end 98 | expect(File.exist?(filename)).to be_falsey 99 | end 100 | end 101 | 102 | after :each do 103 | unstub_resolv_files.call 104 | end 105 | 106 | it 'creates the directory if it doesn\'t exist' do 107 | puts "Requesting sudo to delete #{resolv_dir}".green 108 | Dory::Bash.run_command("sudo rm -rf #{resolv_dir}") 109 | expect{Dory::Resolv::Macos.configure}.to change{Dir.exist?(resolv_dir)}.from(false).to(true) 110 | end 111 | 112 | it "creates the files with the nameserver in it" do 113 | expect(filenames.all?{|f| !File.exist?(f)}).to be_truthy 114 | Dory::Resolv::Macos.configure 115 | expect(filenames.all? do |f| 116 | File.exist?(f) && File.read(f) =~ /added.by.dory/ 117 | end).to be_truthy 118 | end 119 | 120 | it "cleans up properly" do 121 | filenames.each do |filename| 122 | expect{Dory::Resolv::Macos.configure}.to change{ 123 | File.exist?(filename) 124 | }.from(false).to(true) 125 | expect{Dory::Resolv::Macos.clean}.to change{ 126 | File.exist?(filename) 127 | }.from(true).to(false) 128 | end 129 | end 130 | 131 | context 'with dinghy' do 132 | let(:stub_for_dinghy) do 133 | ->(nameserver, port) do 134 | allow(Dory::Dinghy).to receive(:ip) { nameserver } 135 | allow(Dory::Config).to receive(:settings) do 136 | { dory: { resolv: { nameserver: 'dinghy', port: port }}} 137 | end 138 | expect(Dory::Resolv::Macos.nameserver).to eq(nameserver) 139 | end 140 | end 141 | 142 | it 'still knows it has our nameserver if the dinghy ip address changes' do 143 | expect(filenames.all?{|f| !File.exist?(f)}).to be_truthy 144 | 145 | nameserver_1 = '3.3.3.3' 146 | nameserver_2 = '4.4.4.4' 147 | port = 1234 148 | stub_for_dinghy.call(nameserver_1, port) 149 | expect(Dory::Resolv::Macos.nameserver).to eq(nameserver_1) 150 | 151 | Dory::Resolv::Macos.configure 152 | expect(filenames.all? do |f| 153 | File.exist?(f) && File.read(f) =~ /added.by.dory/ 154 | end).to be_truthy 155 | expect(Dory::Resolv::Macos.nameserver).to eq(nameserver_1) 156 | expect(Dory::Resolv::Macos.has_our_nameserver?).to be_truthy 157 | 158 | stub_for_dinghy.call(nameserver_2, port) 159 | expect(Dory::Resolv::Macos.nameserver).to eq(nameserver_2) 160 | expect(Dory::Resolv::Macos.has_our_nameserver?).to be_truthy 161 | 162 | filenames.each{ |filename| expect(File.exist?(filename)).to be_truthy } 163 | Dory::Resolv::Macos.clean 164 | filenames.each{ |filename| expect(File.exist?(filename)).to be_falsey } 165 | end 166 | end 167 | end 168 | 169 | context "Seeing system settings" do 170 | it "knows if we are in the resolv file" do 171 | # TODO check to see if the changes we wrote to the resolv file 172 | # were propagated into the system resolv file 173 | end 174 | 175 | it "knows if we are not in the resolv file" do 176 | 177 | end 178 | end 179 | 180 | context "knows if we've edited the file" do 181 | let(:comment) { '# added by dory' } 182 | 183 | let(:stub_resolv) do 184 | ->(nameserver, file_comment = comment) do 185 | allow(Dory::Resolv::Macos).to receive(:nameserver){ nameserver } 186 | allow(Dory::Resolv::Macos).to receive(:file_comment){ file_comment } 187 | expect(Dory::Resolv::Macos.nameserver).to eq(nameserver) 188 | expect(Dory::Resolv::Macos.file_comment).to eq(file_comment) 189 | end 190 | end 191 | 192 | let(:contents) do 193 | ->(nameserver, port) do 194 | <<-EOF.gsub(' ' * 10, '') 195 | # added by dory 196 | nameserver #{nameserver} 197 | port #{port} 198 | EOF 199 | end 200 | end 201 | 202 | let(:stub_the_things) do 203 | ->(nameserver, port) do 204 | allow(Dory::Resolv::Macos).to receive(:file_nameserver_line) { "nameserver #{nameserver}" } 205 | allow(Dory::Resolv::Macos).to receive(:port) { port } 206 | end 207 | end 208 | 209 | %w[127.0.0.1 192.168.53.164].each do |nameserver| 210 | %w[53 9965 1234].each do |port| 211 | it "does think we edited the file if 127.0.0.1 is there but the comment isn't" do 212 | stub_resolv.call(nameserver) 213 | stub_the_things.call(nameserver, port) 214 | expect( 215 | Dory::Resolv::Macos.contents_has_our_nameserver?(contents.call(nameserver, port)) 216 | ).to be_truthy 217 | end 218 | end 219 | end 220 | 221 | it 'doesnt think we edited the file if we didnt' do 222 | pending 'implement me plz' 223 | fail 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /lib/dory/dnsmasq.rb: -------------------------------------------------------------------------------- 1 | require_relative 'docker_service' 2 | 3 | module Dory 4 | class Dnsmasq 5 | extend Dory::DockerService 6 | 7 | # 8 | # I really hate these globals. It would be great to refactor these out 9 | # 10 | @@first_attempt_failed = false 11 | @@handle_systemd_services = [] 12 | 13 | def self.dnsmasq_image_name 14 | setting = Dory::Config.settings[:dory][:dnsmasq][:image] 15 | setting ? setting : 'freedomben/dory-dnsmasq:1.1.0' 16 | end 17 | 18 | def self.first_attempt_failed? 19 | @@first_attempt_failed ||= false if @first_attempt_failed.nil? 20 | @@first_attempt_failed 21 | end 22 | 23 | def self.set_first_attempt_failed(failed) 24 | @@first_attempt_failed = failed 25 | end 26 | 27 | def self.systemd_services? 28 | return false unless self.systemd_services 29 | self.systemd_services.count > 0 30 | end 31 | 32 | def self.systemd_services 33 | @@systemd_services ||= [] 34 | @@systemd_services 35 | end 36 | 37 | def self.set_systemd_services(services) 38 | @@systemd_services = services 39 | end 40 | 41 | def self.run_preconditions 42 | puts "[DEBUG] dnsmasq service running preconditions" if Dory::Config.debug? 43 | 44 | # we don't want to hassle the user with checking the port unless necessary 45 | if first_attempt_failed? 46 | self.set_systemd_services(self.running_services_that_block_dnsmasq) 47 | self.down_systemd_services if self.systemd_services? 48 | 49 | puts "[DEBUG] First attempt failed. Checking port #{self.port}" if Dory::Config.debug? 50 | listener_list = Dory::PortUtils.check_port(self.port) 51 | unless listener_list.empty? 52 | return self.offer_to_kill(listener_list) 53 | end 54 | 55 | return false 56 | else 57 | puts "[DEBUG] Skipping preconditions on first run" if Dory::Config.debug? 58 | return true 59 | end 60 | end 61 | 62 | def self.run_postconditions 63 | puts "[DEBUG] dnsmasq service running postconditions" if Dory::Config.debug? 64 | self.up_systemd_services if self.systemd_services? 65 | end 66 | 67 | def self.handle_error(_command_output) 68 | puts "[DEBUG] handling dnsmasq start error" if Dory::Config.debug? 69 | # If we've already tried to handle failure, prevent infinite recursion 70 | if first_attempt_failed? 71 | puts "[DEBUG] Attempt to kill conflicting service failed" if Dory::Config.debug? 72 | return false 73 | else 74 | if Dory::Config.debug? 75 | puts "[DEBUG] First attempt to start dnsmasq failed." \ 76 | "There is probably a conflicting service present" 77 | end 78 | set_first_attempt_failed(true) 79 | self.start(handle_error: false) 80 | end 81 | end 82 | 83 | def self.ip_from_dinghy? 84 | Dory::Dinghy.match?(self.address(self.old_address)) || 85 | self.domains.any?{ |domain| Dory::Dinghy.match?(self.address(domain[:address])) } 86 | end 87 | 88 | def self.port 89 | return 53 unless Os.macos? 90 | p = Dory::Config.settings[:dory][:dnsmasq][:port] 91 | p.nil? || p == 0 ? 19323 : self.sanitize_port(p) 92 | end 93 | 94 | def self.sanitize_port(port) 95 | port.to_s.gsub(/\D/, '').to_i 96 | end 97 | 98 | def self.container_name 99 | Dory::Config.settings[:dory][:dnsmasq][:container_name] 100 | end 101 | 102 | def self.domains 103 | Dory::Config.settings[:dory][:dnsmasq][:domains] 104 | end 105 | 106 | def self.old_domain 107 | Dory::Config.settings[:dory][:dnsmasq][:domain] 108 | end 109 | 110 | def self.old_address 111 | Dory::Config.settings[:dory][:dnsmasq][:address] 112 | end 113 | 114 | def self.address(addr) 115 | Dory::Dinghy.match?(addr) ? Dory::Dinghy.ip : addr 116 | end 117 | 118 | def self.domain_addr_arg_string 119 | if self.old_domain 120 | "#{Shellwords.escape(self.old_domain)} #{Shellwords.escape(self.address(self.old_address))}" 121 | else 122 | self.domains.map do |domain| 123 | "#{Shellwords.escape(domain[:domain])} #{Shellwords.escape(self.address(domain[:address]))}" 124 | end.join(" ") 125 | end 126 | end 127 | 128 | def self.run_command 129 | "docker run -d -p #{self.port}:#{self.port}/tcp -p #{self.port}:#{self.port}/udp " \ 130 | "--name=#{Shellwords.escape(self.container_name)} " \ 131 | "--cap-add=NET_ADMIN #{Shellwords.escape(self.dnsmasq_image_name)} " \ 132 | "#{self.domain_addr_arg_string}" 133 | end 134 | 135 | def self.offer_to_kill(listener_list, answer: nil) 136 | listener_list.each do |process| 137 | puts "Process '#{process.command}' with PID '#{process.pid}' is listening on #{process.node} port #{self.port}.".yellow 138 | end 139 | pids = listener_list.uniq(&:pid).map(&:pid) 140 | pidstr = pids.join(' and ') 141 | print "This interferes with Dory's dnsmasq container. Would you like me to kill PID #{pidstr}? (Y/N): ".yellow 142 | conf = answer ? answer : answer_from_settings 143 | conf = STDIN.gets.chomp unless conf 144 | if conf =~ /y/i 145 | puts "Requesting sudo to kill PID #{pidstr}".green 146 | return Sh.run_command("sudo kill #{pids.join(' ')}").success? 147 | else 148 | puts "OK, not killing PID #{pidstr}. Please kill manually and try starting dory again.".red 149 | return false 150 | end 151 | end 152 | 153 | def self.services_that_block_dnsmasq 154 | %w[ 155 | NetworkManager.service 156 | systemd-resolved.service 157 | ] 158 | end 159 | 160 | def self.has_services_that_block_dnsmasq? 161 | !self.running_services_that_block_dnsmasq.empty? 162 | end 163 | 164 | def self.running_services_that_block_dnsmasq 165 | self.services_that_block_dnsmasq.select do |service| 166 | Dory::Systemd.systemd_service_running?(service) 167 | end 168 | end 169 | 170 | def self.down_systemd_services 171 | puts "[DEBUG] Putting systemd services down" if Dory::Config.debug? 172 | 173 | conf = if ask_about_killing? 174 | puts "You have some systemd services running that will race against us \n" \ 175 | "to bind to port 53 (and usually they win):".yellow 176 | puts "\n #{self.systemd_services.join(', ')}\n".yellow 177 | puts "If we don't stop these services temporarily while putting up the \n" \ 178 | "dnsmasq container, starting it will likely fail.".yellow 179 | print "Would you like me to put them down while we start dns \n" \ 180 | "(I'll put them back up when finished)? (Y/N): ".yellow 181 | STDIN.gets.chomp 182 | else 183 | answer_from_settings 184 | end 185 | if conf =~ /y/i 186 | if self.systemd_services.all? { |service| 187 | Dory::Systemd.set_systemd_service(service: service, up: false) 188 | } 189 | puts "Putting down services succeeded".green 190 | else 191 | puts "One or more services failed to stop".red 192 | end 193 | else 194 | puts 'OK, not putting down the services'.yellow 195 | set_systemd_services([]) 196 | end 197 | end 198 | 199 | def self.up_systemd_services 200 | if self.systemd_services? 201 | puts "[DEBUG] Putting systemd services back up: #{self.systemd_services.join(', ')}" if Dory::Config.debug? 202 | if self.systemd_services.reverse.all? { |service| 203 | Dory::Systemd.set_systemd_service(service: service, up: true) 204 | } 205 | puts "#{self.systemd_services.join(', ')} were successfully restarted".green 206 | else 207 | puts "#{self.systemd_services.join(', ')} failed to restart".red 208 | end 209 | else 210 | puts "[DEBUG] Not putting systemd services back up cause array was empty " if Dory::Config.debug? 211 | end 212 | end 213 | 214 | def self.ask_about_killing? 215 | !self.answer_from_settings 216 | end 217 | 218 | def self.kill_others 219 | Dory::Config.settings[:dory][:dnsmasq][:kill_others] 220 | end 221 | 222 | def self.answer_from_settings 223 | # This `== true` is important because kill_others could be 224 | # 'no' which would be a truthy value despite the fact that it 225 | # should be falsey 226 | if self.kill_others == true || self.kill_others =~ /yes/i 227 | 'Y' 228 | elsif self.kill_others == false || self.kill_others =~ /no/i 229 | 'N' 230 | else 231 | nil 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /spec/lib/config_spec.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | 3 | RSpec.describe Dory::Config do 4 | context "main" do 5 | let(:filename) { '/tmp/dory-test-config-yml' } 6 | 7 | let(:ssl_certs_dir) { '/usr/bin' } 8 | let(:proxy_container_name) { 'dory_dinghy_http_proxy_test_name' } 9 | let(:overridden_proxy_container_name) { 'some_container_name' } 10 | 11 | let(:default_config) do 12 | %Q( 13 | --- 14 | :dory: 15 | :dnsmasq: 16 | :enabled: true 17 | :domains: 18 | - :domain: docker_test_name 19 | :address: 192.168.11.1 20 | - :domain: docker_second_test 21 | :address: 192.168.11.3 22 | :container_name: dory_dnsmasq_test_name 23 | :nginx_proxy: 24 | :enabled: true 25 | :ssl_certs_dir: #{ssl_certs_dir} 26 | :container_name: #{proxy_container_name} 27 | :resolv: 28 | :enabled: true 29 | :nameserver: 192.168.11.1 30 | ).split("\n").map{|s| s.sub(' ' * 8, '')}.join("\n") 31 | end 32 | 33 | let(:incomplete_config) do 34 | %Q( 35 | --- 36 | :dory: 37 | :dnsmasq: 38 | :enabled: true 39 | :domain: docker_test_name 40 | :address: 192.168.11.1 41 | :container_name: dory_dnsmasq_test_name 42 | :nginx_proxy: 43 | :enabled: true 44 | :container_name: #{overridden_proxy_container_name} 45 | :resolv: 46 | :enabled: true 47 | :nameserver: 192.168.11.1 48 | ).split("\n").map{|s| s.sub(' ' * 8, '')}.join("\n") 49 | end 50 | 51 | let(:upgradeable_config) do 52 | %Q( 53 | --- 54 | :dory: 55 | :dnsmasq: 56 | :enabled: true 57 | :domain: docker_test_name 58 | :address: 192.168.11.1 59 | :container_name: dory_dnsmasq_test_name 60 | :nginx_proxy: 61 | :enabled: true 62 | :ssl_certs_dir: #{ssl_certs_dir} 63 | :container_name: #{proxy_container_name} 64 | :resolv: 65 | :enabled: true 66 | :nameserver: 192.168.11.1 67 | ).split("\n").map{|s| s.sub(' ' * 8, '')}.join("\n") 68 | end 69 | 70 | let(:somewhat_complete_config) do 71 | %Q( 72 | --- 73 | :dory: 74 | :dnsmasq: 75 | :enabled: true 76 | :domain: docker_test_name 77 | :address: 192.168.11.1 78 | :container_name: dory_dnsmasq_test_name 79 | :service_start_delay: 9 80 | :kill_others: yes 81 | :nginx_proxy: 82 | :enabled: true 83 | :ssl_certs_dir: #{ssl_certs_dir} 84 | :container_name: #{proxy_container_name} 85 | :resolv: 86 | :enabled: true 87 | :nameserver: 192.168.11.1 88 | ).split("\n").map{|s| s.sub(' ' * 8, '')}.join("\n") 89 | end 90 | 91 | before :each do 92 | allow(Dory::Config).to receive(:filename) { filename } 93 | allow(Dory::Config).to receive(:default_yaml) { default_config } 94 | end 95 | 96 | after :each do 97 | File.delete(filename) if File.exist?(filename) 98 | end 99 | 100 | it "let's you override settings" do 101 | Dory::Config.write_default_settings_file 102 | test_addr = "3.3.3.3" 103 | new_config = YAML.load(default_config) 104 | new_config[:dory][:dnsmasq][:domains][0][:address] = test_addr 105 | Dory::Config.write_settings(new_config, filename, is_yaml: false) 106 | expect(File.exist?(filename)).to be_truthy 107 | expect(Dory::Config.settings[:dory][:dnsmasq][:domains][0][:address]).to eq(test_addr) 108 | expect(Dory::Config.settings[:dory][:dnsmasq][:domains][0][:domain]).to eq('docker_test_name') 109 | end 110 | 111 | it "doesn't squash defaults if they're missing in the config file" do 112 | Dory::Config.write_settings(incomplete_config, filename, is_yaml: true) 113 | expect(File.exist?(filename)).to be_truthy 114 | 115 | settings = Dory::Config.default_settings 116 | expect(settings[:dory][:nginx_proxy].keys).to include('ssl_certs_dir') 117 | expect(settings[:dory][:nginx_proxy][:ssl_certs_dir]).to eq(ssl_certs_dir) 118 | expect(settings[:dory][:nginx_proxy][:container_name]).to eq(proxy_container_name) 119 | 120 | settings = Dory::Config.settings 121 | expect(settings[:dory][:nginx_proxy].keys).to include('ssl_certs_dir') 122 | expect(settings[:dory][:nginx_proxy][:ssl_certs_dir]).to eq(ssl_certs_dir) 123 | expect(settings[:dory][:nginx_proxy][:container_name]).to eq(overridden_proxy_container_name) 124 | end 125 | 126 | context "debug mode" do 127 | it "can be put in debug mode" do 128 | Dory::Config.write_default_settings_file 129 | new_config = YAML.load(default_config) 130 | new_config[:dory][:debug] = true 131 | Dory::Config.write_settings(new_config, filename, is_yaml: false) 132 | expect(File.exist?(filename)).to be_truthy 133 | expect(Dory::Config.debug?).to be_truthy 134 | end 135 | 136 | it "defaults to non-debug mode" do 137 | expect(Dory::Config.debug?).to be_falsey 138 | end 139 | end 140 | 141 | it "fixes domain/address in upgrade" do 142 | Dory::Config.write_settings(upgradeable_config, filename, is_yaml: true) 143 | Dory::Config.upgrade_settings_file(filename) 144 | new_settings = Dory::Config.settings 145 | expect(new_settings[:dory][:dnsmasq]).not_to have_key(:domain) 146 | expect(new_settings[:dory][:dnsmasq]).not_to have_key(:address) 147 | expect(new_settings[:dory][:dnsmasq][:domains].length).to eq(1) 148 | expect(new_settings[:dory][:dnsmasq][:domains][0][:domain]).to eq('docker_test_name') 149 | expect(new_settings[:dory][:dnsmasq][:domains][0][:address]).to eq('192.168.11.1') 150 | end 151 | 152 | it "adds nginx proxy port to config file in upgrade" do 153 | Dory::Config.write_settings(upgradeable_config, filename, is_yaml: true) 154 | Dory::Config.upgrade_settings_file(filename) 155 | new_settings = Dory::Config.settings 156 | expect(new_settings[:dory][:nginx_proxy][:port]).to eq(80) 157 | expect(new_settings[:dory][:nginx_proxy][:tls_port]).to eq(443) 158 | end 159 | 160 | it "uses hashes with indifferent access" do 161 | Dory::Config.write_default_settings_file 162 | test_addr = "3.3.3.3" 163 | new_config = YAML.load(default_config) 164 | new_config[:dory][:dnsmasq][:domains][0][:address] = test_addr 165 | Dory::Config.write_settings(new_config, filename, is_yaml: false) 166 | expect(File.exist?(filename)).to be_truthy 167 | expect(Dory::Config.settings[:dory][:dnsmasq][:domains][0][:address]).to eq(test_addr) 168 | expect(Dory::Config.settings[:dory][:dnsmasq][:domains][0][:domain]).to eq('docker_test_name') 169 | expect(Dory::Config.settings['dory']['dnsmasq']['domains'][0]['address']).to eq(test_addr) 170 | expect(Dory::Config.settings['dory']['dnsmasq']['domains'][0]['domain']).to eq('docker_test_name') 171 | end 172 | 173 | it "adds the kill setting defaulted to 'ask'" do 174 | Dory::Config.write_settings(upgradeable_config, filename, is_yaml: true) 175 | Dory::Config.upgrade_settings_file(filename) 176 | new_settings = Dory::Config.settings 177 | expect(new_settings[:dory][:dnsmasq]).to have_key(:kill_others) 178 | expect(new_settings[:dory][:dnsmasq][:kill_others]).to eq('ask') 179 | end 180 | 181 | it "doesn't change kill settings if they exist" do 182 | Dory::Config.write_settings(somewhat_complete_config, filename, is_yaml: true) 183 | Dory::Config.upgrade_settings_file(filename) 184 | new_settings = Dory::Config.settings 185 | expect(new_settings[:dory][:dnsmasq]).to have_key(:kill_others) 186 | pending 'YAML.load_file() read "yes" into true :-(' 187 | expect(new_settings[:dory][:dnsmasq][:kill_others]).to eq('yes') 188 | end 189 | 190 | it "adds service_start_delay to config" do 191 | Dory::Config.write_settings(upgradeable_config, filename, is_yaml: true) 192 | Dory::Config.upgrade_settings_file(filename) 193 | new_settings = Dory::Config.settings 194 | expect(new_settings[:dory][:dnsmasq]).to have_key(:service_start_delay) 195 | expect(new_settings[:dory][:dnsmasq][:service_start_delay]).to eq(5) 196 | end 197 | 198 | it "doesn't change existing service_start_delay" do 199 | Dory::Config.write_settings(somewhat_complete_config, filename, is_yaml: true) 200 | Dory::Config.upgrade_settings_file(filename) 201 | new_settings = Dory::Config.settings 202 | expect(new_settings[:dory][:dnsmasq]).to have_key(:service_start_delay) 203 | expect(new_settings[:dory][:dnsmasq][:service_start_delay]).to eq(9) 204 | end 205 | end 206 | 207 | context "dynamic config file locations" do 208 | it "defaults to /home/[user]/.dory.yml if no file exists" do 209 | expect(Dory::Config.filename).to eq("#{Dir.home}/.dory.yml") 210 | end 211 | 212 | it "finds .dory.yml in the current directory" do 213 | Dir.mktmpdir do |tmpdir| 214 | dory_dot_yml = "#{tmpdir}/.dory.yml" 215 | expect{File.write(dory_dot_yml, "ohai")}.to change{File.exist?(dory_dot_yml)}.from(false).to(true) 216 | Dir.chdir(tmpdir) do 217 | expect(Dory::Config.filename).to eql(dory_dot_yml) 218 | end 219 | end 220 | end 221 | 222 | it "finds .dory.yml in the parent directory" do 223 | Dir.mktmpdir do |tmpdir| 224 | childdir = "#{tmpdir}/child" 225 | dory_dot_yml = "#{tmpdir}/.dory.yml" 226 | expect{File.write(dory_dot_yml, "ohai")}.to change{File.exist?(dory_dot_yml)}.from(false).to(true) 227 | Dir.mkdir(childdir) 228 | Dir.chdir(childdir) do 229 | expect(Dory::Config.filename).to eql(dory_dot_yml) 230 | end 231 | end 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /spec/lib/systemd_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::Systemd do 2 | let(:cups_disabled_not_running) do 3 | return <<-EOF 4 | ● cups.service - CUPS Scheduler 5 | Loaded: loaded (/usr/lib/systemd/system/cups.service; disabled; vendor preset: enabled) 6 | Active: inactive (dead) since Mon 2017-02-13 15:29:13 AKST; 1min 29s ago 7 | Docs: man:cupsd(8) 8 | Main PID: 3043 (code=exited, status=0/SUCCESS) 9 | Status: "Scheduler is running..." 10 | 11 | Feb 13 05:26:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 12 | Feb 13 06:25:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 13 | Feb 13 07:23:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 14 | Feb 13 08:21:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 15 | Feb 13 09:20:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 16 | Feb 13 10:18:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 17 | Feb 13 11:16:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 18 | Feb 13 15:26:00 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 19 | Feb 13 15:29:13 ben systemd[1]: Stopping CUPS Scheduler... 20 | Feb 13 15:29:13 ben systemd[1]: Stopped CUPS Scheduler. 21 | EOF 22 | end 23 | 24 | let(:cups_enabled_not_running_retval) do 25 | OpenStruct.new(success?: false, exitstatus: 3, stdout: cups_enabled_not_running) 26 | end 27 | 28 | let(:cups_enabled_not_running) do 29 | <<-EOF 30 | ● cups.service - CUPS Scheduler 31 | Loaded: loaded (/usr/lib/systemd/system/cups.service; enabled; vendor preset: enabled) 32 | Active: inactive (dead) since Mon 2017-02-13 15:29:13 AKST; 1s ago 33 | Docs: man:cupsd(8) 34 | Main PID: 3043 (code=exited, status=0/SUCCESS) 35 | Status: "Scheduler is running..." 36 | 37 | Feb 13 05:26:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 38 | Feb 13 06:25:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 39 | Feb 13 07:23:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 40 | Feb 13 08:21:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 41 | Feb 13 09:20:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 42 | Feb 13 10:18:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 43 | Feb 13 11:16:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 44 | Feb 13 15:26:00 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 45 | Feb 13 15:29:13 ben systemd[1]: Stopping CUPS Scheduler... 46 | Feb 13 15:29:13 ben systemd[1]: Stopped CUPS Scheduler. 47 | EOF 48 | end 49 | 50 | let(:cups_enabled_and_running_retval) do 51 | OpenStruct.new(success?: true, stdout: cups_enabled_and_running) 52 | end 53 | 54 | let(:cups_enabled_and_running) do 55 | <<-EOF 56 | ● cups.service - CUPS Scheduler 57 | Loaded: loaded (/usr/lib/systemd/system/cups.service; enabled; vendor preset: enabled) 58 | Active: active (running) since Fri 2017-02-10 17:38:08 AKST; 2 days ago 59 | Docs: man:cupsd(8) 60 | Main PID: 3043 (cupsd) 61 | Status: "Scheduler is running..." 62 | Tasks: 1 (limit: 512) 63 | CGroup: /system.slice/cups.service 64 | └─3043 /usr/sbin/cupsd -l 65 | 66 | Feb 13 03:30:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 67 | Feb 13 04:28:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 68 | Feb 13 05:26:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 69 | Feb 13 06:25:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 70 | Feb 13 07:23:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 71 | Feb 13 08:21:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 72 | Feb 13 09:20:18 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 73 | Feb 13 10:18:38 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 74 | Feb 13 11:16:58 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 75 | Feb 13 15:26:00 ben cupsd[3043]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 76 | EOF 77 | end 78 | 79 | let(:cups_not_installed_retval) do 80 | OpenStruct.new(success?: false, exitstatus: 3, stdout: cups_not_installed) 81 | end 82 | 83 | let(:cups_not_installed) do 84 | <<-EOF 85 | ● cups.service 86 | Loaded: not-found (Reason: No such file or directory) 87 | Active: inactive (dead) 88 | EOF 89 | end 90 | 91 | let(:cups_disabled_and_running) do 92 | <<-EOF 93 | ● cups.service - CUPS Scheduler 94 | Loaded: loaded (/usr/lib/systemd/system/cups.service; disabled; vendor preset: enabled) 95 | Active: active (running) since Mon 2017-02-13 16:24:20 AKST; 19h ago 96 | Docs: man:cupsd(8) 97 | Main PID: 1201 (cupsd) 98 | Status: "Scheduler is running..." 99 | Tasks: 1 (limit: 512) 100 | CGroup: /system.slice/cups.service 101 | └─1201 /usr/sbin/cupsd -l 102 | 103 | Feb 14 02:39:21 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 104 | Feb 14 03:37:41 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 105 | Feb 14 04:36:01 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 106 | Feb 14 05:34:21 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 107 | Feb 14 06:32:41 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 108 | Feb 14 07:31:01 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 109 | Feb 14 08:29:21 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 110 | Feb 14 09:27:41 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 111 | Feb 14 10:26:01 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 112 | Feb 14 11:24:21 ben cupsd[1201]: REQUEST localhost - - "POST / HTTP/1.1" 200 1 113 | EOF 114 | end 115 | 116 | describe '#has_systemd?' do 117 | let(:which_not_found) do 118 | '/usr/bin/which: no systemctl in (/home/ben/.gem/ruby/2.4.0/bin:/home/ben/.' \ 119 | 'rubies/ruby-2.4.0/lib/ruby/gems/2.4.0/bin:/home/ben/.rubies/ruby-2.4.0/bin:' \ 120 | '/home/ben/.linuxbrew/bin:/home/ben/.nvm/versions/node/v6.4.0/bin:' \ 121 | '/home/ben/.linuxbrew/bin:/home/ben/.linuxbrew/bin:/home/ben/.linuxbrew/bin:' \ 122 | '/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:' \ 123 | '/usr/local/sbin:/home/ben/bin:/home/ben/go/bin:/usr/local/sbin:/home/ben/go/bin)' 124 | end 125 | 126 | it 'knows if systemd is installed' do 127 | expect(Dory::Sh).to receive(:run_command).with('which systemctl') do 128 | OpenStruct.new(success?: true, stdout: '/usr/bin/systemctl') 129 | end 130 | expect(Dory::Systemd).to have_systemd 131 | end 132 | 133 | it 'knows if systemd is NOT installed' do 134 | expect(Dory::Sh).to receive(:run_command).with('which systemctl') do 135 | OpenStruct.new(success?: false, stdout: which_not_found, exitstatus: 1) 136 | end 137 | expect(Dory::Systemd).not_to have_systemd 138 | end 139 | end 140 | 141 | describe '#systemd_service_installed?' do 142 | it 'knows cups is installed' do 143 | [ 144 | cups_enabled_not_running, 145 | cups_enabled_and_running, 146 | cups_disabled_not_running 147 | ].each do |cups| 148 | allow(Dory::Sh).to receive(:run_command) do 149 | OpenStruct.new(success?: true, stdout: cups) 150 | end 151 | expect(Dory::Systemd.systemd_service_installed?('cups')).to be_truthy 152 | end 153 | end 154 | 155 | # we can do this without stubbing once travis is on a distro with systemd 156 | it 'knows cups is not installed' do 157 | allow(Dory::Sh).to receive(:run_command) { cups_not_installed_retval } 158 | expect(Dory::Systemd.systemd_service_installed?('cups')).to be_falsey 159 | end 160 | end 161 | 162 | describe '#systemd_service_running?' do 163 | it 'knows cups is running' do 164 | allow(Dory::Sh).to receive(:run_command) do 165 | OpenStruct.new(success?: true, stdout: cups_enabled_and_running) 166 | end 167 | expect(Dory::Systemd.systemd_service_running?('cups')).to be_truthy 168 | end 169 | 170 | it 'knows cups is not running' do 171 | allow(Dory::Sh).to receive(:run_command) do 172 | OpenStruct.new(success?: true, stdout: cups_enabled_not_running) 173 | end 174 | expect(Dory::Systemd.systemd_service_running?('cups')).to be_falsey 175 | end 176 | 177 | it 'doesnt think non-installed services are running' do 178 | allow(Dory::Sh).to receive(:run_command) { cups_not_installed_retval } 179 | expect(Dory::Systemd.systemd_service_running?('cups')).to be_falsey 180 | end 181 | end 182 | 183 | describe '#systemd_service_enabled?' do 184 | it 'knows cups is enabled' do 185 | allow(Dory::Sh).to receive(:run_command) do 186 | OpenStruct.new(success?: true, stdout: cups_enabled_and_running) 187 | end 188 | expect(Dory::Systemd.systemd_service_enabled?('cups')).to be_truthy 189 | end 190 | 191 | it 'knows cups is enabled even if not running' do 192 | allow(Dory::Sh).to receive(:run_command) do 193 | OpenStruct.new(success?: true, stdout: cups_enabled_not_running) 194 | end 195 | expect(Dory::Systemd.systemd_service_enabled?('cups')).to be_truthy 196 | end 197 | 198 | it 'knows cups is disabled' do 199 | allow(Dory::Sh).to receive(:run_command) do 200 | OpenStruct.new(success?: true, stdout: cups_disabled_not_running) 201 | end 202 | expect(Dory::Systemd.systemd_service_enabled?('cups')).to be_falsey 203 | end 204 | 205 | it 'knows cups is disabled even if running' do 206 | allow(Dory::Sh).to receive(:run_command) do 207 | OpenStruct.new(success?: true, stdout: cups_disabled_and_running) 208 | end 209 | expect(Dory::Systemd.systemd_service_enabled?('cups')).to be_falsey 210 | end 211 | end 212 | 213 | describe '#set_systemd_service' do 214 | let(:stub_systemd_call) do 215 | # on systems without systemd we have to stub this 216 | ->(now_running) do 217 | if (now_running) 218 | allow(Dory::Sh).to receive(:run_command) { cups_enabled_and_running_retval } 219 | else 220 | allow(Dory::Sh).to receive(:run_command) { cups_enabled_not_running_retval } 221 | end 222 | end 223 | end 224 | 225 | it 'puts the service down' do 226 | do_the_stubs = !Dory::Systemd.has_systemd? 227 | stub_systemd_call.call(true) if do_the_stubs 228 | Dory::Systemd.set_systemd_service(service: 'cups', up: true) 229 | expect { 230 | stub_systemd_call.call(false) if do_the_stubs 231 | Dory::Systemd.set_systemd_service(service: 'cups', up: false) 232 | }.to change { 233 | Dory::Systemd.systemd_service_running?('cups') 234 | }.from(true).to(false) 235 | end 236 | 237 | it 'brings the service up' do 238 | do_the_stubs = !Dory::Systemd.has_systemd? 239 | stub_systemd_call.call(false) if do_the_stubs 240 | Dory::Systemd.set_systemd_service(service: 'cups', up: false) 241 | expect { 242 | stub_systemd_call.call(true) if do_the_stubs 243 | Dory::Systemd.set_systemd_service(service: 'cups', up: true) 244 | }.to change { 245 | Dory::Systemd.systemd_service_running?('cups') 246 | }.from(false).to(true) 247 | end 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /spec/lib/dnsmasq_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dory::Dnsmasq do 2 | let(:dory_config) do 3 | %q(--- 4 | :dory: 5 | :dnsmasq: 6 | :enabled: true 7 | :domains: 8 | - :domain: docker_test_name 9 | :address: 192.168.11.1 10 | - :domain: docker_second 11 | :address: 192.168.11.2 12 | :container_name: dory_dnsmasq_test_name 13 | :kill_others: false 14 | ).split("\n").map{|s| s.sub(' ' * 6, '')}.join("\n") 15 | end 16 | 17 | let(:dory_old_config) do 18 | %q(--- 19 | :dory: 20 | :dnsmasq: 21 | :enabled: true 22 | :domain: docker_test_name 23 | :address: 192.168.11.1 24 | :container_name: dory_dnsmasq_test_name 25 | ).split("\n").map{|s| s.sub(' ' * 6, '')}.join("\n") 26 | end 27 | 28 | let(:dory_config_dinghy) do 29 | %q(--- 30 | :dory: 31 | :dnsmasq: 32 | :enabled: true 33 | :domains: 34 | - :domain: docker_test_name 35 | :address: dinghy 36 | - :domain: docker_second 37 | :address: dingy # not an accident 38 | :container_name: dory_dnsmasq_test_name 39 | ).split("\n").map{|s| s.sub(' ' * 6, '')}.join("\n") 40 | end 41 | 42 | def start_service_on_53 43 | puts "Requesting sudo to start an ncat listener on 53" 44 | `sudo echo 'Got sudo. starting ncat listener'` 45 | Process.spawn('sudo ncat -l 53') 46 | sleep(0.5) # give the process time to bind 47 | end 48 | 49 | def cleanup_53 50 | Dory::Sh.run_command('sudo killall ncat') 51 | Dory::Sh.run_command('sudo killall exe') 52 | end 53 | 54 | before :all do 55 | cleanup_53 56 | end 57 | 58 | after :all do 59 | Dory::Dnsmasq.delete 60 | end 61 | 62 | it "has the docker client" do 63 | expect(Dory::Dnsmasq.docker_installed?).to be_truthy 64 | end 65 | 66 | it "respects settings (Using defaults)" do 67 | allow(Dory::Config).to receive(:filename) { "/tmp/doesnotexist.lies" } 68 | allow(Dory::Config).to receive(:default_yaml) { dory_config } 69 | expect(Dory::Dnsmasq.container_name).to eq('dory_dnsmasq_test_name') 70 | expect(Dory::Dnsmasq.domains).to eq([ 71 | { 'domain' => 'docker_test_name', 'address' => '192.168.11.1' }, 72 | { 'domain' => 'docker_second', 'address' => '192.168.11.2' } 73 | ]) 74 | end 75 | 76 | it "starts up the container" do 77 | Dory::Dnsmasq.delete 78 | expect(Dory::Dnsmasq).not_to be_running 79 | expect{Dory::Dnsmasq.start}.to change{Dory::Dnsmasq.running?}.from(false).to(true) 80 | expect(Dory::Dnsmasq).to be_container_exists 81 | end 82 | 83 | it "doesn't fail when starting the container twice" do 84 | 2.times{ expect{Dory::Dnsmasq.start}.not_to raise_error } 85 | expect(Dory::Dnsmasq).to be_running 86 | end 87 | 88 | it "deletes the container" do 89 | expect(Dory::Dnsmasq.start).to be_truthy 90 | expect(Dory::Dnsmasq).to be_running 91 | expect(Dory::Dnsmasq).to be_container_exists 92 | expect{Dory::Dnsmasq.delete}.to change{Dory::Dnsmasq.container_exists?}.from(true).to(false) 93 | expect(Dory::Dnsmasq).not_to be_running 94 | end 95 | 96 | it "stops the container" do 97 | expect(Dory::Dnsmasq.start).to be_truthy 98 | expect(Dory::Dnsmasq).to be_running 99 | expect(Dory::Dnsmasq.stop).to be_truthy 100 | expect(Dory::Dnsmasq).to be_container_exists 101 | expect(Dory::Dnsmasq).not_to be_running 102 | end 103 | 104 | it "starts the container when it already exists" do 105 | expect(Dory::Dnsmasq.start).to be_truthy 106 | expect(Dory::Dnsmasq).to be_running 107 | expect(Dory::Dnsmasq.stop).to be_truthy 108 | expect(Dory::Dnsmasq).to be_container_exists 109 | expect{Dory::Dnsmasq.start}.to change{Dory::Dnsmasq.running?}.from(false).to(true) 110 | expect(Dory::Dnsmasq).to be_container_exists 111 | expect(Dory::Dnsmasq).to be_running 112 | end 113 | 114 | it "handles an old (single) domain properly" do 115 | allow(Dory::Config).to receive(:filename) { "/tmp/doesnotexist.lies" } 116 | allow(Dory::Config).to receive(:default_yaml) { dory_old_config } 117 | expect(Dory::Dnsmasq.old_domain).to eq('docker_test_name') 118 | expect(Dory::Dnsmasq.old_address).to eq('192.168.11.1') 119 | expect(Dory::Dnsmasq.domain_addr_arg_string).to eq('docker_test_name 192.168.11.1') 120 | end 121 | 122 | it "handles an array of domains properly" do 123 | allow(Dory::Config).to receive(:filename) { "/tmp/doesnotexist.lies" } 124 | allow(Dory::Config).to receive(:default_yaml) { dory_config } 125 | expect(Dory::Dnsmasq.old_domain).to be_nil 126 | expect(Dory::Dnsmasq.old_address).to be_nil 127 | expect(Dory::Dnsmasq.domain_addr_arg_string).to eq( 128 | 'docker_test_name 192.168.11.1 docker_second 192.168.11.2' 129 | ) 130 | end 131 | 132 | it "slurps the ip from dinghy if set" do 133 | allow(Dory::Config).to receive(:filename) { "/tmp/doesnotexist.lies" } 134 | allow(Dory::Config).to receive(:default_yaml) { dory_config_dinghy } 135 | allow(Dory::Dinghy).to receive(:ip) { '1.1.1.1' } 136 | expect(Dory::Dnsmasq.address('dinghy')).to eq('1.1.1.1') 137 | expect(Dory::Dnsmasq.domain_addr_arg_string).to match(/1\.1\.1\.1/) 138 | end 139 | 140 | it "fails if dinghy doesn't return an ip address" do 141 | allow(Dory::Config).to receive(:filename) { "/tmp/doesnotexist.lies" } 142 | allow(Dory::Config).to receive(:default_yaml) { dory_config_dinghy } 143 | allow(Dory::Bash).to receive(:run_command) { OpenStruct.new(stdout: "something totally wrong\n") } 144 | expect{ Dory::Dnsmasq.address('dinghy') }.to raise_error(Dory::Dinghy::DinghyError) 145 | expect{ Dory::Dnsmasq.domain_addr_arg_string }.to raise_error(Dory::Dinghy::DinghyError) 146 | end 147 | 148 | context 'pre-existing listener on 53' do 149 | let(:port) { 53 } 150 | 151 | before(:all) { expect(Dory::Dnsmasq.stop).to be_truthy } 152 | 153 | before(:each) { start_service_on_53 } 154 | after(:each) { cleanup_53 } 155 | 156 | it "kills listening services" do 157 | expect(Dory::PortUtils.check_port(port)).not_to be_empty 158 | expect(Dory::Dnsmasq.offer_to_kill(Dory::PortUtils.check_port(port), answer: 'y')).to be_truthy 159 | expect(Dory::PortUtils.check_port(port)).to be_empty 160 | end 161 | 162 | it "doesn't kill the listening services if declined" do 163 | expect(Dory::PortUtils.check_port(port)).not_to be_empty 164 | expect(Dory::Dnsmasq.offer_to_kill(Dory::PortUtils.check_port(port), answer: 'n')).to be_falsey 165 | expect(Dory::PortUtils.check_port(port)).not_to be_empty 166 | end 167 | end 168 | 169 | context 'specified port' do 170 | let(:stub_settings) do 171 | ->(new_settings) do 172 | allow(Dory::Config).to receive(:settings) { new_settings } 173 | end 174 | end 175 | 176 | context 'linux' do 177 | it 'always returns port 53 on linux' do 178 | port = 53 179 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker' }}}) 180 | expect(Dory::Dnsmasq.port).to eq(port) 181 | expect(Dory::Dnsmasq.run_command).to match(/-p.#{port}:#{port}\/tcp.-p.#{port}:#{port}\/udp/) 182 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: 9999 }}}) 183 | expect(Dory::Dnsmasq.port).to eq(port) 184 | expect(Dory::Dnsmasq.run_command).to match(/-p.#{port}:#{port}\/tcp.-p.#{port}:#{port}\/udp/) 185 | end 186 | end 187 | 188 | context 'macos' do 189 | before(:each) do 190 | allow(Dory::Os).to receive(:macos?) { true } 191 | end 192 | 193 | after(:each) do 194 | allow(Dory::Os).to receive(:macos?).and_call_original 195 | end 196 | 197 | it 'defaults port to 19323 on macos' do 198 | stub_settings.call({ dory: { dnsmasq: {}}}) 199 | expect(Dory::Dnsmasq.port).to eq(19323) 200 | end 201 | 202 | it 'respects the port setting on macos' do 203 | port = 9999 204 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: port }}}) 205 | expect(Dory::Dnsmasq.port).to eq(port) 206 | expect(Dory::Dnsmasq.run_command).to match(/-p.#{port}:#{port}\/tcp.-p.#{port}:#{port}\/udp/) 207 | end 208 | 209 | it 'sanitizes the port input' do 210 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: '9999' }}}) 211 | expect(Dory::Dnsmasq.port).to eq(9999) 212 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: '999a' }}}) 213 | expect(Dory::Dnsmasq.port).to eq(999) 214 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: '999;' }}}) 215 | expect(Dory::Dnsmasq.port).to eq(999) 216 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: '999"' }}}) 217 | expect(Dory::Dnsmasq.port).to eq(999) 218 | stub_settings.call({ dory: { dnsmasq: { domain: 'docker', port: '999\'' }}}) 219 | expect(Dory::Dnsmasq.port).to eq(999) 220 | end 221 | end 222 | end 223 | 224 | context 'kill others setting' do 225 | let(:stub_settings) do 226 | ->(new_settings) do 227 | allow(Dory::Config).to receive(:settings) { new_settings } 228 | end 229 | end 230 | 231 | it '#kill_others pulls the settings out' do 232 | stub_settings.call({ dory: { dnsmasq: { kill_others: 'yo' }}}) 233 | expect(Dory::Dnsmasq.kill_others).to eq('yo') 234 | end 235 | 236 | { 237 | true => 'Y', 238 | 'yes' => 'Y', 239 | false => 'N', 240 | 'no' => 'N', 241 | 'ask' => nil, 242 | 'something-wrong' => nil 243 | }.each do |value, expected| 244 | it "#answer_from_settings handles #{value}" do 245 | stub_settings.call({ dory: { dnsmasq: { kill_others: value }}}) 246 | expect(Dory::Dnsmasq.answer_from_settings).to eq(expected) 247 | end 248 | 249 | it "#ask_about_killing handles #{value}" do 250 | stub_settings.call({ dory: { dnsmasq: { kill_others: value }}}) 251 | expect(Dory::Dnsmasq.ask_about_killing?).to eq(expected == nil) 252 | end 253 | end 254 | end 255 | 256 | context 'smoke test' do 257 | it 'runs the command (smoke test)' do 258 | got_called = false 259 | allow(Dory::Systemd).to receive(:has_systemd?) { false } 260 | allow(Dory::Dnsmasq).to receive(:delete_container_if_exists) { true } 261 | allow(Dory::Dnsmasq).to receive(:run_preconditions) { true } 262 | allow(Dory::Dnsmasq).to receive(:run_postconditions) { true } 263 | allow(Dory::Sh).to receive(:run_command) do 264 | got_called = true 265 | OpenStruct.new(success?: true) 266 | end 267 | expect { 268 | Dory::Dnsmasq.execute_run_command(handle_error: true) 269 | }.to change{ got_called }.from(false).to(true) 270 | end 271 | end 272 | 273 | context 'methods that save state' do 274 | context 'first attempt failed' do 275 | it 'returns false if not initialized' do 276 | expect(Dory::Dnsmasq.first_attempt_failed?).to eq(false) 277 | end 278 | 279 | it 'reads and writes' do 280 | expect { Dory::Dnsmasq.set_first_attempt_failed(true) }.to change{ 281 | Dory::Dnsmasq.first_attempt_failed? 282 | }.from(false).to(true) 283 | expect { Dory::Dnsmasq.set_first_attempt_failed(false) }.to change{ 284 | Dory::Dnsmasq.first_attempt_failed? 285 | }.from(true).to(false) 286 | end 287 | end 288 | 289 | context 'systemd service list' do 290 | let(:test_list1) { %w[a-service b-service] } 291 | let(:test_list2) { %w[a-service b-service c-service] } 292 | 293 | it 'defaults to empty array when not initialized' do 294 | expect(Dory::Dnsmasq.systemd_services).to eq([]) 295 | expect(Dory::Dnsmasq.systemd_services?).to eq(false) 296 | end 297 | 298 | it 'reads and writes' do 299 | expect { Dory::Dnsmasq.set_systemd_services(test_list1) }.to change{ 300 | Dory::Dnsmasq.systemd_services 301 | }.from([]).to(test_list1) 302 | expect { Dory::Dnsmasq.set_systemd_services(test_list2) }.to change{ 303 | Dory::Dnsmasq.systemd_services 304 | }.from(test_list1).to(test_list2) 305 | expect { Dory::Dnsmasq.set_systemd_services([]) }.to change{ 306 | Dory::Dnsmasq.systemd_services 307 | }.from(test_list2).to([]) 308 | end 309 | end 310 | end 311 | 312 | context 'custom settings' do 313 | let(:stub_settings) do 314 | ->(new_settings) do 315 | allow(Dory::Config).to receive(:settings) { new_settings } 316 | end 317 | end 318 | 319 | context 'custom image' do 320 | let(:default_image_name) { 'freedomben/dory-dnsmasq:1.1.0' } 321 | let(:new_image_name) { 'some/awesome-image:1.0.0' } 322 | 323 | it 'allows setting a custom image' do 324 | expect(Dory::Dnsmasq.dnsmasq_image_name).to eq(default_image_name) 325 | stub_settings.call({ dory: { dnsmasq: { image: new_image_name }}}) 326 | expect(Dory::Dnsmasq.dnsmasq_image_name).to eq(new_image_name) 327 | end 328 | end 329 | end 330 | end 331 | -------------------------------------------------------------------------------- /spec/bin/dory_spec.rb: -------------------------------------------------------------------------------- 1 | load 'bin/dory' 2 | 3 | RSpec.describe DoryBin do 4 | def run_with(*args) 5 | DoryBin.start(args) 6 | end 7 | 8 | # This method will wrap the call and return stdout as a string. 9 | def capture_stdout 10 | begin 11 | old_stdout = $stdout 12 | $stdout = StringIO.new('','w') 13 | yield 14 | $stdout.string 15 | ensure 16 | $stdout = old_stdout 17 | end 18 | end 19 | 20 | def resolv_dir 21 | '/tmp/resolver' 22 | end 23 | 24 | def macos_resolv_filenames 25 | %w[/tmp/resolver/docker /tmp/resolver/dory] 26 | end 27 | 28 | def linux_resolv_filename 29 | '/tmp/resolv.conf' 30 | end 31 | 32 | def config_filename 33 | '/tmp/dory-test-config-yml' 34 | end 35 | 36 | def default_config 37 | %Q( 38 | --- 39 | :dory: 40 | :dnsmasq: 41 | :enabled: true 42 | :domains: 43 | - :domain: docker_test_name 44 | :address: 192.168.11.1 45 | - :domain: docker_second_test 46 | :address: 192.168.11.3 47 | :container_name: dory_dnsmasq_test_name 48 | :nginx_proxy: 49 | :enabled: true 50 | :ssl_certs_dir: #{ssl_certs_dir} 51 | :container_name: dory_dinghy_http_proxy_test_name 52 | :resolv: 53 | :enabled: true 54 | :nameserver: 192.168.11.1 55 | ).split("\n").map{|s| s.sub(' ' * 6, '')}.join("\n") 56 | end 57 | 58 | let(:dory_bin) { DoryBin.new } 59 | 60 | let(:set_macos) do 61 | ->() do 62 | allow(Dory::Os).to receive(:macos?){ true } 63 | allow(Dory::Os).to receive(:ubuntu?){ false } 64 | allow(Dory::Os).to receive(:fedora?){ false } 65 | allow(Dory::Os).to receive(:arch?){ false } 66 | end 67 | end 68 | 69 | let(:unset_macos) do 70 | ->() do 71 | allow(Dory::Os).to receive(:macos?).and_call_original 72 | allow(Dory::Os).to receive(:ubuntu?).and_call_original 73 | allow(Dory::Os).to receive(:fedora?).and_call_original 74 | allow(Dory::Os).to receive(:arch?).and_call_original 75 | end 76 | end 77 | 78 | let(:set_docker_installed) do 79 | ->(installed) do 80 | allow(Dory::DockerService).to receive(:docker_installed?) { installed } 81 | end 82 | end 83 | 84 | let(:restore_docker_installed) do 85 | ->() do 86 | allow(Dory::Dnsmasq).to receive(:docker_installed?).and_call_original 87 | allow(Dory::Proxy).to receive(:docker_installed?).and_call_original 88 | end 89 | end 90 | 91 | let(:ssl_certs_dir) { '/usr/bin' } 92 | let(:overridden_proxy_container_name) { 'some_container_name' } 93 | 94 | before :all do 95 | File.delete(config_filename) if File.exist?(config_filename) 96 | `touch #{linux_resolv_filename}` 97 | end 98 | 99 | after :all do 100 | File.delete(config_filename) if File.exist?(config_filename) 101 | end 102 | 103 | before :each do 104 | allow(Dory::Config).to receive(:filename) { config_filename } 105 | allow(Dory::Config).to receive(:default_yaml) { default_config } 106 | allow(Dory::Resolv::Macos).to receive(:resolv_files) { macos_resolv_filenames } 107 | allow(Dory::Resolv::Linux).to receive(:common_resolv_file) { linux_resolv_filename } 108 | allow(Dory::Resolv::Linux).to receive(:ubuntu_resolv_file) { linux_resolv_filename } 109 | end 110 | 111 | after :each do 112 | # delete any containers we made so we don't pollute other tests 113 | Dory::Dnsmasq.delete 114 | Dory::Proxy.delete 115 | end 116 | 117 | describe 'up' do 118 | %w[proxy dns resolv].each do |service| 119 | it "only starts #{service} when specified by itself" do 120 | allow(Dory::Proxy).to receive(:start) { service == 'proxy' } 121 | allow(Dory::Dnsmasq).to receive(:start) { service == 'dns' } 122 | allow(Dory::Resolv).to receive(:configure) { service == 'resolv' } 123 | dory_bin.up(service) 124 | expect(Dory::Proxy).send(service == 'proxy' ? :to : :not_to, have_received(:start)) 125 | expect(Dory::Dnsmasq).send(service == 'dns' ? :to : :not_to, have_received(:start)) 126 | expect(Dory::Resolv).send(service == 'resolv' ? :to : :not_to, have_received(:configure)) 127 | end 128 | end 129 | end 130 | 131 | describe 'down' do 132 | %w[proxy dns resolv].each do |service| 133 | it "only stops #{service} when specified by itself" do 134 | allow(Dory::Proxy).to receive(:stop) { service == 'proxy' } 135 | allow(Dory::Dnsmasq).to receive(:stop) { service == 'dns' } 136 | allow(Dory::Resolv).to receive(:clean) { service == 'resolv' } 137 | dory_bin.down(service) 138 | expect(Dory::Proxy).send(service == 'proxy' ? :to : :not_to, have_received(:stop)) 139 | expect(Dory::Dnsmasq).send(service == 'dns' ? :to : :not_to, have_received(:stop)) 140 | expect(Dory::Resolv).send(service == 'resolv' ? :to : :not_to, have_received(:clean)) 141 | end 142 | end 143 | end 144 | 145 | describe 'version' do 146 | it 'answers with the version' do 147 | expect(capture_stdout{dory_bin.version}).to match(/dory.*version.*#{Dory.version}/i) 148 | end 149 | end 150 | 151 | describe 'restart' do 152 | %w[proxy dns resolv].each do |service| 153 | it "only stops #{service} when specified by itself" do 154 | allow(Dory::Proxy).to receive(:start) { service == 'proxy' } 155 | allow(Dory::Dnsmasq).to receive(:start) { service == 'dns' } 156 | allow(Dory::Resolv).to receive(:configure) { service == 'resolv' } 157 | allow(Dory::Proxy).to receive(:stop) { service == 'proxy' } 158 | allow(Dory::Dnsmasq).to receive(:stop) { service == 'dns' } 159 | allow(Dory::Resolv).to receive(:clean) { service == 'resolv' } 160 | dory_bin.restart(service) 161 | expect(Dory::Proxy).send(service == 'proxy' ? :to : :not_to, have_received(:stop)) 162 | expect(Dory::Dnsmasq).send(service == 'dns' ? :to : :not_to, have_received(:stop)) 163 | expect(Dory::Resolv).send(service == 'resolv' ? :to : :not_to, have_received(:clean)) 164 | expect(Dory::Proxy).send(service == 'proxy' ? :to : :not_to, have_received(:start)) 165 | expect(Dory::Dnsmasq).send(service == 'dns' ? :to : :not_to, have_received(:start)) 166 | expect(Dory::Resolv).send(service == 'resolv' ? :to : :not_to, have_received(:configure)) 167 | end 168 | end 169 | end 170 | 171 | describe 'status' do 172 | it 'doesnt crash' do 173 | expect{capture_stdout{dory_bin.status}}.not_to raise_error 174 | expect{capture_stdout{dory_bin.up}}.not_to raise_error 175 | expect{capture_stdout{dory_bin.status}}.not_to raise_error 176 | expect{capture_stdout{dory_bin.down}}.not_to raise_error 177 | expect{capture_stdout{dory_bin.ip}}.not_to raise_error 178 | set_macos.call 179 | expect{capture_stdout{dory_bin.status}}.not_to raise_error 180 | expect{capture_stdout{dory_bin.up}}.not_to raise_error 181 | expect{capture_stdout{dory_bin.status}}.not_to raise_error 182 | expect{capture_stdout{dory_bin.down}}.not_to raise_error 183 | expect{capture_stdout{dory_bin.ip}}.not_to raise_error 184 | unset_macos.call 185 | end 186 | end 187 | 188 | describe 'pull' do 189 | it 'prints error message when docker is not installed' do 190 | set_docker_installed.call(false) 191 | expect{capture_stdout{dory_bin.pull}}.not_to raise_error 192 | expect(capture_stdout{dory_bin.pull}).to match(/docker.*not.*installed/i) 193 | end 194 | 195 | it 'pulls down the images' do 196 | set_docker_installed.call(true) 197 | results = capture_stdout{dory_bin.pull} 198 | expect(results).not_to match(/docker.*not.*installed/i) 199 | expect(results).to match(/pulling.image/i) 200 | end 201 | end 202 | 203 | describe 'ip' do 204 | %w[dns proxy].each do |cont| 205 | it "prints the ip address of the #{cont} container" do 206 | expect(capture_stdout{dory_bin.ip(cont)}).to match(/172\.\d{1,3}\.\d{1,3}\.\d{1,3}/i) 207 | end 208 | end 209 | end 210 | 211 | describe 'config file' do 212 | let(:config_file_exists?) { ->() { File.exist?(Dory::Config.filename)}} 213 | 214 | let(:old_config) do 215 | %Q(--- 216 | :dory: 217 | :dnsmasq: 218 | :enabled: true 219 | :domain: docker_test_name 220 | :address: 192.168.11.1 221 | :container_name: dory_dnsmasq_test_name 222 | :nginx_proxy: 223 | :enabled: true 224 | :container_name: nginx_container_name 225 | :resolv: 226 | :enabled: true 227 | :nameserver: 192.168.11.1 228 | ).split("\n").map{|s| s.sub(' ' * 6, '')}.join("\n") 229 | end 230 | 231 | it 'writes a config file' do 232 | expect{ run_with('config-file') }.to change{ config_file_exists?.call }.from(false).to(true) 233 | end 234 | 235 | context 'upgrades' do 236 | before :each do 237 | File.write(config_filename, old_config) 238 | run_with('config-file', '--upgrade') 239 | end 240 | 241 | it 'converts symbol keys to strings' do 242 | yaml = YAML.load_file(config_filename) 243 | yaml.each_key do |k| 244 | expect(k).to be_a(String) 245 | yaml[k].each_key{ |ck| expect(ck).to be_a(String) } 246 | end 247 | end 248 | 249 | it 'moves domain and address into array' do 250 | yaml = YAML.load_file(config_filename).with_indifferent_access 251 | expect(yaml[:dory][:dnsmasq][:domain]).to be_nil 252 | expect(yaml[:dory][:dnsmasq][:address]).to be_nil 253 | expect(yaml[:dory][:dnsmasq][:domains].length).to eq(1) 254 | expect(yaml[:dory][:dnsmasq][:domains][0][:domain]).to eq('docker_test_name') 255 | expect(yaml[:dory][:dnsmasq][:domains][0][:address]).to eq('192.168.11.1') 256 | end 257 | end 258 | 259 | context 'config_file_action' do 260 | it 'doesn\'t ask about upgrading if flag is set' do 261 | expect(dory_bin.send('config_file_action', { upgrade: true })).to eq('u') 262 | end 263 | 264 | it 'doesn\'t ask about overwriting if flag is set' do 265 | expect(dory_bin.send('config_file_action', { force: true })).to eq('o') 266 | end 267 | 268 | it 'infers upgrade if both upgrade and force are set' do 269 | expect(dory_bin.send('config_file_action', { upgrade: true, force: true })).to eq('u') 270 | end 271 | end 272 | end 273 | 274 | context 'services enabled/disabled' do 275 | %I[nginx_proxy dnsmasq resolv].each do |service| 276 | context "#{service}" do 277 | [{ enabled: true}, { disabled: false }].each do |enabled| 278 | it "is #{enabled.keys.first} when #{enabled.keys.first}" do 279 | settings = { dory: { service => { enabled: enabled.values.first }}} 280 | expect(dory_bin.send("#{service}_enabled?", (settings))).to eq(enabled.values.first) 281 | end 282 | end 283 | end 284 | end 285 | end 286 | 287 | describe 'specification of services' do 288 | context 'sanitization' do 289 | it 'defaults to all services if none are specified' do 290 | expect(dory_bin.send(:sanitize_services, [])).to eq(dory_bin.send(:valid_services)) 291 | end 292 | 293 | it 'returns false if an invalid service is present' do 294 | expect(dory_bin.send(:sanitize_services, ['badness'])).to be_falsey 295 | end 296 | 297 | it 'returns a list of canonical services' do 298 | input = %w[nginx-proxy resolve dnsmasq] 299 | output = %w[proxy resolv dns] 300 | expect(dory_bin.send(:sanitize_services, input)).to match_array(output) 301 | end 302 | end 303 | 304 | context 'canonicalization of services' do 305 | it 'figures out what you mean' do 306 | { 307 | 'proxy' => 'proxy', 308 | 'nginx' => 'proxy', 309 | 'nginx_proxy' => 'proxy', 310 | 'nginx-proxy' => 'proxy', 311 | 'dns' => 'dns', 312 | 'dnsmasq' => 'dns', 313 | 'resolv' => 'resolv', 314 | 'resolve' => 'resolv' 315 | }.each do |input, can| 316 | expect(dory_bin.send(:canonical_service, input)).to eq(can) 317 | end 318 | end 319 | end 320 | 321 | it 'validates the service' do 322 | { 323 | 'proxy' => true, 324 | 'nginx' => true, 325 | 'nginx_proxy' => true, 326 | 'nginx-proxy' => true, 327 | 'dns' => true, 328 | 'dnsmasq' => true, 329 | 'resolv' => true, 330 | 'resolve' => true, 331 | 'wrong' => false, 332 | 'hello' => false, 333 | 'world' => false 334 | }.each do |input, valid| 335 | expect(dory_bin.send(:valid_service?, input)). to eq(valid) 336 | end 337 | end 338 | end 339 | 340 | context "doesn't crash when docker is not installed" do 341 | %w[status up down restart version pull].each do |method| 342 | it " - #{method}" do 343 | set_docker_installed.call(false) 344 | expect{capture_stdout{dory_bin.send(method)}}.not_to raise_error 345 | restore_docker_installed.call 346 | end 347 | end 348 | end 349 | end 350 | -------------------------------------------------------------------------------- /bin/dory: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'thor' 4 | require 'yaml' 5 | require 'json' 6 | 7 | require 'dory' 8 | 9 | class DoryBin < Thor 10 | class_option :verbose, type: :boolean, aliases: 'v', default: false 11 | 12 | desc 'upgrade', 'Upgrade dory to the latest version' 13 | long_desc <<-LONGDESC 14 | Upgrades dory to the latest version. Old versions are cleaned up 15 | 16 | If dory was installed with sudo, this may not work. You will 17 | have to do it manually: 18 | 19 | sudo gem install dory # install the latest versions 20 | sudo gem cleanup dory # cleanup old versions 21 | 22 | > $ dory upgrade 23 | LONGDESC 24 | def upgrade 25 | exec_upgrade(options) 26 | end 27 | 28 | desc 'up', 'Bring up dory services (nginx-proxy, dnsmasq, resolv)' 29 | long_desc <<-LONGDESC 30 | Bring up dory services (nginx-proxy, dnsmasq, resolv) 31 | 32 | When run, the docker container for the nginx proxy is started, 33 | along with a local dnsmasq instance to resolve DNS requests 34 | for your custom domain to the nginx proxy. The local resolver 35 | will also be configured to use the dnsmasq instance as a nameserver 36 | 37 | Optionally, you can pass a list of service names as arguments 38 | to have only one or two services started. 39 | 40 | > $ dory up [proxy] [dns] [resolv] 41 | LONGDESC 42 | def up(*services) 43 | exec_up(options, services) 44 | end 45 | 46 | desc 'down', 'Stop all dory services' 47 | long_desc <<-LONGDESC 48 | Stops all dory services. Can optionally pass [-d|--destroy] 49 | to destroy the containers when they stop. 50 | 51 | Optionally, you can pass a list of service names as arguments 52 | to have only one or two services started. 53 | 54 | > $ dory down [-d|--destroy] [proxy] [dns] [resolv] 55 | LONGDESC 56 | option :destroy, type: :boolean, aliases: 'd', default: true 57 | def down(*services) 58 | exec_down(options, services) 59 | end 60 | 61 | desc 'version', 'Check current installed version of dory' 62 | def version 63 | puts "Dory - Version: #{Dory.version}" 64 | end 65 | 66 | desc 'restart', 'Stop and restart all dory services' 67 | long_desc <<-LONGDESC 68 | Stop and restart dory services (nginx-proxy, dnsmasq, resolv) 69 | 70 | > $ dory restart [-d|--destroy] 71 | LONGDESC 72 | option :destroy, type: :boolean, aliases: 'd', default: true 73 | def restart(*services) 74 | exec_down(options, services) 75 | exec_up(options, services) 76 | end 77 | 78 | desc 'status', 'Report status of the dory services' 79 | long_desc <<-LONGDESC 80 | Checks the current status of the services managed by dory. 81 | This includes nginx-proxy, dnsmasq, and resolv 82 | 83 | > $ dory status 84 | LONGDESC 85 | def status 86 | exec_status(options) 87 | end 88 | 89 | desc 'config-file', 'Write a default config file' 90 | long_desc <<-LONGDESC 91 | Writes a dory config file to #{Dory::Config.filename} 92 | containing the default settings. This can then be configured 93 | as preferred. 94 | 95 | > $ dory config-file [--upgrade] [--force] 96 | LONGDESC 97 | option :upgrade, type: :boolean, aliases: 'u', default: false 98 | option :force, type: :boolean, aliases: 'f', default: false 99 | def config_file 100 | exec_config_file(options) 101 | end 102 | 103 | desc 'attach', 'Attach to the output of a docker service container' 104 | long_desc <<-LONGDESC 105 | For the nginx proxy and dnsmasq containers, this command 106 | provides a pass-through to the underlying docker attach command. 107 | It is simply more convenient than looking up the docker 108 | container name and using the equivalent 'docker attach' 109 | 110 | > $ dory attach [proxy] [dns] 111 | LONGDESC 112 | def attach(service = nil) 113 | exec_attach(service, options) 114 | end 115 | 116 | desc 'pull', 'Pull down the docker images that dory uses' 117 | long_desc <<-LONGDESC 118 | This will pull down the docker images that dory uses. 119 | You don't need to run this command as dory will automatically 120 | pull down the images when needed if they aren't there already. 121 | However, you may wish to pull down in advance, and for that 122 | purpose this command is here for you. 123 | 124 | > $ dory pull [proxy] [dns] 125 | LONGDESC 126 | def pull(*services) 127 | exec_pull(services, options) 128 | end 129 | 130 | desc 'ip', 'Grab the IPv4 address of a running dory service' 131 | long_desc <<-LONGDESC 132 | For the nginx proxy and dnsmasq containers, this command 133 | grabs the IP address of the container. You could get this 134 | info from `docker inspect` but this is more convenient. 135 | 136 | > $ dory ip [proxy] [dns] 137 | LONGDESC 138 | def ip(service = nil) 139 | exec_ip(service, options) 140 | end 141 | 142 | private 143 | 144 | def self.exit_on_failure? 145 | true 146 | end 147 | 148 | def exec_pull(services, _options) 149 | servs = services.empty? ? %w[proxy dns] : services 150 | servs = sanitize_services(servs) 151 | return unless servs 152 | 153 | unless Dory::DockerService.docker_installed? 154 | puts "Docker does not appear to be installed /o\\".red 155 | puts "Docker is required for DNS and Nginx proxy. These can be " \ 156 | "disabled in the config file if you don't need them.\n".yellow 157 | puts "Please install docker and try again".red 158 | return 159 | end 160 | 161 | servs.each do |service| 162 | imgname = if service == 'proxy' 163 | Dory::Proxy.dory_http_proxy_image_name 164 | elsif service == 'dns' 165 | Dory::Dnsmasq.dnsmasq_image_name 166 | else 167 | nil 168 | end 169 | if imgname 170 | puts "Pulling image '#{imgname}'...".green 171 | if Dory::Sh.run_command("docker pull #{imgname}").success? 172 | puts "Successfully pulled image '#{imgname}'".green 173 | else 174 | puts "Error pulling docker image '#{imgname}'".red 175 | end 176 | else 177 | puts "Can only pull 'proxy' or 'dns', you tried to pull '#{service}'".red 178 | end 179 | end 180 | end 181 | 182 | def exec_ip(service, _options) 183 | s = sanitize_service(service) 184 | mod = if s == 'proxy' 185 | Dory::Proxy 186 | elsif s == 'dns' 187 | Dory::Dnsmasq 188 | else 189 | nil 190 | end 191 | if mod 192 | unless mod.running? 193 | puts "Service '#{service}' is not running. Starting...".green 194 | if mod.start 195 | puts "Service '#{service}' successfully started.".green 196 | else 197 | puts "Error starting service '#{service}'".red 198 | end 199 | end 200 | if mod.running? 201 | inspect = Dory::Sh.run_command("docker inspect #{mod.container_name}") 202 | if inspect.success? 203 | begin 204 | puts JSON.parse(inspect.stdout).first['NetworkSettings']['IPAddress'] 205 | rescue StandardError => e 206 | puts "Error parsing JSON from container #{mod.container_name}:".red 207 | puts e.message.red 208 | end 209 | else 210 | puts "Error inspecting docker container #{mod.container_name}!".red 211 | end 212 | else 213 | puts "Service '#{service}' is not running. Cannot get IP address".red 214 | end 215 | else 216 | puts "Can only get IP address for 'proxy' or 'dns'".red 217 | end 218 | end 219 | 220 | def exec_attach(service, _options) 221 | s = sanitize_service(service) 222 | mod = if s == 'proxy' 223 | Dory::Proxy 224 | elsif s == 'dns' 225 | Dory::Dnsmasq 226 | else 227 | nil 228 | end 229 | if mod 230 | unless mod.running? 231 | puts "Service '#{service}' is not running. Starting...".green 232 | if mod.start 233 | puts "Service '#{service}' successfully started.".green 234 | else 235 | puts "Error starting service '#{service}'".red 236 | end 237 | end 238 | if mod.running? 239 | puts "Note that if you Ctrl + C after attaching,".yellow 240 | puts "the service will need to be restarted.".yellow 241 | puts "Service '#{service}' is running.".green 242 | puts "Attaching to docker container '#{mod.container_name}'...".green 243 | exec("docker attach #{mod.container_name}") 244 | else 245 | puts "Service '#{service}' is not running. Cannot attach".red 246 | end 247 | else 248 | puts "Can only attach to 'proxy' or 'dns'".red 249 | end 250 | end 251 | 252 | def exec_upgrade(_options) 253 | puts "Checking if dory has updates available...".green 254 | new_version = Dory::Upgrade.new_version 255 | if new_version 256 | if Dory::Upgrade.outdated?(new_version) 257 | puts "New version #{new_version} is available. You currently have #{Dory.version}.".yellow 258 | print "Would you like to install the update? (Y/N): ".yellow 259 | if STDIN.gets.chomp =~ /y/i 260 | puts "Upgrading dory...".green 261 | if Dory::Upgrade.install.success? 262 | if Dory::Upgrade.cleanup.success? 263 | puts "New version installed successfully!\n" \ 264 | "You may want to upgrade your config file with:\n\n" \ 265 | " dory config-file --upgrade".green 266 | else 267 | puts "Failure cleaning up old versions of dory. You may want " \ 268 | "to run 'gem cleanup dory' manually.".red 269 | end 270 | else 271 | puts "Failure installing new version of dory. If you are " \ 272 | "installing into a system ruby, this could be because " \ 273 | "you need to use sudo. Please try 'gem install dory' " \ 274 | "manually, and then 'gem cleanup dory' to remove old " \ 275 | "versions.".red 276 | end 277 | else 278 | puts "Not upgrading. User declined.".red 279 | end 280 | else 281 | puts "Dory is up to date! Nothing to do".green 282 | end 283 | else 284 | puts "Encountered an error checking the latest version from Rubygems".red 285 | end 286 | end 287 | 288 | def config_file_action(options) 289 | if options[:upgrade] 290 | 'u' 291 | elsif options[:force] 292 | 'o' 293 | else 294 | 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 295 | STDIN.gets.chomp 296 | end 297 | end 298 | 299 | def take_action(action) 300 | if action =~ /u/i 301 | puts "Upgrading config file at #{Dory::Config.filename}".green 302 | Dory::Config.upgrade_settings_file 303 | elsif action =~ /o/i 304 | puts "Overwriting config file with new version at #{Dory::Config.filename}".green 305 | Dory::Config.write_default_settings_file 306 | else 307 | puts "User declined. Not writing config file".red 308 | return 309 | end 310 | end 311 | 312 | def exec_config_file(options) 313 | if File.exist?(Dory::Config.filename) 314 | take_action(config_file_action(options)) 315 | else 316 | puts "Writing new config file to #{Dory::Config.filename}".green 317 | Dory::Config.write_default_settings_file 318 | end 319 | end 320 | 321 | def exec_up(options, services) 322 | services = sanitize_services(services) 323 | return unless services 324 | 325 | puts "Reading settings file at '#{Dory::Config.filename}'".green if options[:verbose] 326 | settings = Dory::Config.settings 327 | if services.include?('proxy') 328 | if nginx_proxy_enabled?(settings) 329 | puts "nginx_proxy enabled in config file".green if options[:verbose] 330 | if Dory::Proxy.start 331 | puts "Successfully started nginx proxy".green 332 | else 333 | puts "Error starting nginx proxy".red 334 | end 335 | else 336 | puts "nginx_proxy disabled in config file".yellow 337 | end 338 | end 339 | 340 | if services.include?('dns') 341 | if dnsmasq_enabled?(settings) 342 | puts "dnsmasq enabled in config file".green if options[:verbose] 343 | if Dory::Dnsmasq.start 344 | puts "Successfully started dnsmasq".green 345 | else 346 | puts "Error starting dnsmasq".red 347 | end 348 | else 349 | puts "dnsmasq disabled in config file".yellow 350 | end 351 | end 352 | 353 | if services.include?('resolv') 354 | if resolv_enabled?(settings) 355 | if Dory::Resolv.configure 356 | puts "Successfully configured local resolver".green 357 | else 358 | puts "Error configuring local resolver".red 359 | end 360 | puts "resolv enabled in config file".green if options[:verbose] 361 | else 362 | puts "resolv disabled in config file".yellow 363 | end 364 | end 365 | end 366 | 367 | def exec_status(_options) 368 | puts "Reading settings file at '#{Dory::Config.filename}'".green if options[:verbose] 369 | settings = Dory::Config.settings 370 | 371 | if Dory::Proxy.running? 372 | puts "[*] Nginx proxy: Running as docker container #{Dory::Proxy.container_name}".green 373 | elsif !nginx_proxy_enabled?(settings) 374 | puts "[*] Nginx proxy is disabled in config file".yellow 375 | else 376 | puts "[*] Nginx proxy is not running".red 377 | end 378 | 379 | if Dory::Dnsmasq.running? 380 | puts "[*] Dnsmasq: Running as docker container #{Dory::Dnsmasq.container_name}".green 381 | elsif !dnsmasq_enabled?(settings) 382 | puts "[*] Dnsmasq is disabled in config file".yellow 383 | else 384 | puts "[*] Dnsmasq is not running".red 385 | end 386 | 387 | if Dory::Resolv.has_our_nameserver? 388 | puts "[*] Resolv: configured with #{Dory::Resolv.file_nameserver_line}".green 389 | elsif !resolv_enabled?(settings) 390 | puts "[*] Resolv is disabled in config file".yellow 391 | else 392 | puts "[*] Resolv is not configured".red 393 | end 394 | end 395 | 396 | def exec_down(options, services) 397 | services = sanitize_services(services) 398 | return unless services 399 | 400 | puts "Reading settings file at '#{Dory::Config.filename}'".green if options[:verbose] 401 | settings = Dory::Config.settings 402 | 403 | if services.include?('resolv') 404 | if Dory::Resolv.clean 405 | if resolv_enabled?(settings) 406 | puts "nameserver removed from resolv file".green 407 | else 408 | puts "Resolv disabled in config file".yellow 409 | end 410 | else 411 | puts "Unable to remove nameserver from resolv file".red 412 | end 413 | end 414 | 415 | if services.include?('dns') 416 | if Dory::Dnsmasq.stop 417 | if dnsmasq_enabled?(settings) 418 | puts "Dnsmasq container stopped".green 419 | if options[:destroy] 420 | if Dory::Dnsmasq.delete 421 | puts "Dnsmasq container successfully deleted".green 422 | else 423 | puts "Dnsmasq container failed to delete".red 424 | end 425 | end 426 | else 427 | puts "dnsmasq disabled in config file".yellow 428 | end 429 | else 430 | puts "Dnsmasq container failed to stop".red 431 | end 432 | end 433 | 434 | if services.include?('proxy') 435 | if Dory::Proxy.stop 436 | if nginx_proxy_enabled?(settings) 437 | puts "Nginx proxy stopped".green 438 | if options[:destroy] 439 | if Dory::Proxy.delete 440 | puts "Nginx proxy container successfully deleted".green 441 | else 442 | puts "Nginx proxy container failed to delete".red 443 | end 444 | end 445 | else 446 | puts "Nginx proxy disabled in config file".yellow 447 | end 448 | else 449 | puts "Nginx proxy failed to stop".red 450 | end 451 | end 452 | end 453 | 454 | def nginx_proxy_enabled?(settings) 455 | settings[:dory][:nginx_proxy][:enabled] 456 | end 457 | 458 | def nginx_proxy_disabled?(settings) 459 | !nginx_proxy_enabled?(settings) 460 | end 461 | 462 | def dnsmasq_enabled?(settings) 463 | settings[:dory][:dnsmasq][:enabled] 464 | end 465 | 466 | def dnsmasq_disabled?(settings) 467 | !dnsmasq_enabled?(settings) 468 | end 469 | 470 | def resolv_enabled?(settings) 471 | settings[:dory][:resolv][:enabled] 472 | end 473 | 474 | def resolv_disabled?(settings) 475 | !resolv_enabled?(settings) 476 | end 477 | 478 | def valid_services 479 | %w[proxy dns resolv] 480 | end 481 | 482 | def canonical_service(service) 483 | { 484 | 'proxy' => 'proxy', 485 | 'nginx' => 'proxy', 486 | 'nginx_proxy' => 'proxy', 487 | 'nginx-proxy' => 'proxy', 488 | 'dns' => 'dns', 489 | 'dnsmasq' => 'dns', 490 | 'resolv' => 'resolv', 491 | 'resolve' => 'resolv' 492 | }[service] 493 | end 494 | 495 | def valid_service?(service) 496 | valid_services.include?(canonical_service(service)) 497 | end 498 | 499 | def valid_services?(services) 500 | services.all? do |service| 501 | if valid_service?(service) 502 | true 503 | else 504 | puts "'#{service}' is not valid. Must be one or more of these: #{valid_services.join(', ')}".red 505 | false 506 | end 507 | end 508 | end 509 | 510 | def sanitize_service(service) 511 | return false if service.nil? || service.empty? 512 | return false unless valid_service?(service) 513 | canonical_service(service) 514 | end 515 | 516 | def sanitize_services(services) 517 | return valid_services if !services || services.empty? 518 | return false unless valid_services?(services) 519 | services.map{|s| canonical_service(s) } 520 | end 521 | end 522 | 523 | aliases = { 524 | 'start' => 'up', 525 | 'stop' => 'down', 526 | 'update' => 'upgrade' 527 | } 528 | 529 | if !ARGV.empty? && %w[-v --version].include?(ARGV.first) 530 | puts "Dory - Version: #{Dory.version}" 531 | else 532 | DoryBin.start(ARGV.map { |a| aliases.keys.include?(a) ? aliases[a] : a }) 533 | end 534 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dory 2 | 3 | [![Gem Version](https://badge.fury.io/rb/dory.svg)](https://badge.fury.io/rb/dory) [![Build Status](https://travis-ci.org/FreedomBen/dory.svg?branch=master)](https://travis-ci.org/FreedomBen/dory) [![Code Climate](https://codeclimate.com/github/FreedomBen/dory/badges/gpa.svg)](https://codeclimate.com/github/FreedomBen/dory) [![Test Coverage](https://codeclimate.com/github/FreedomBen/dory/badges/coverage.svg)](https://codeclimate.com/github/FreedomBen/dory/coverage) [![Dependency Status](https://dependencyci.com/github/FreedomBen/dory/badge)](https://dependencyci.com/github/FreedomBen/dory) 4 | 5 | [Dory](https://github.com/FreedomBen/dory) lets you forget about IP addresses and port numbers 6 | while you are developing your application. Through the magic of local DNS and 7 | a reverse proxy, you can access your app at the domain of your choosing. For example, 8 | http://myapp.docker or http://this-is-a-really-long-name.but-its-cool-cause-i-like-it 9 | 10 | Now with support for Docker for Mac and even [dinghy! (more info on using with dinghy below)](#usage-with-dinghy) 11 | 12 | Dory wraps [codekitchen/dinghy-http-proxy](https://github.com/codekitchen/dinghy-http-proxy) 13 | and makes it easily available for use outside of [dinghy](https://github.com/codekitchen/dinghy). 14 | This way you can work comfortably side by side with your colleagues who run dinghy on macOS. 15 | 16 | Specifically, dory will: 17 | 18 | * Fire up the nginx proxy in a daemonized docker container for you 19 | * Configure and start a local dnsmasq to forward DNS queries for 20 | your local domain to the nginx proxy 21 | * Configure your local DNS resolver to point to the local dnsmasq 22 | 23 | ## Installation 24 | 25 | A package for .deb and .rpm is planned as well as a systemd service. 26 | If you'd like to help out with any of that, let me know! 27 | 28 | ### [Homebrew](https://brew.sh) (recommended on macOS) 29 | 30 | ```bash 31 | brew install dory 32 | ``` 33 | 34 | ### Ruby gem (recommended on Linux) 35 | 36 | **NOTE:** Dory requires ruby version 2.2 or greater to be installed on your system already. 37 | 38 | If you use multiple versions, your system ruby is too old or you just prefer not to install gems into your system ruby, I recommend installing the ruby version with [ruby-install](https://github.com/postmodern/ruby-install) and then managing it with [chruby](https://github.com/postmodern/chruby). 39 | 40 | ```bash 41 | gem install dory 42 | ``` 43 | 44 | ### Arch Linux (hosted on the [quarry repository](https://wiki.archlinux.org/index.php/Unofficial_user_repositories#quarry)) 45 | 46 | ```bash 47 | pacman -S ruby-dory 48 | ``` 49 | 50 | ## Quick Start 51 | 52 | In most cases, the default configuration will be all you need. You literally 53 | just [set the VIRTUAL_HOST environment variable in your container](#making-your-containers-accessible-by-name-dns), 54 | [install dory](#installation) and then run: 55 | 56 | dory up 57 | 58 | If you want to fine-tune, generate a config file with: 59 | 60 | dory config-file 61 | 62 | and edit away at `~/.dory.yml` 63 | 64 | ## Usage 65 | 66 | Dory has a small selection of commands that are hopefully intuitive. 67 | To customize and fine-tune dory's behavior, it can be configured with a yaml config file. 68 | 69 | ### Commands 70 | ``` 71 | Commands: 72 | dory attach # Attach to the output of a docker service container 73 | dory config-file # Write a default config file 74 | dory down # Stop all dory services 75 | dory help [COMMAND] # Describe available commands or one specific command 76 | dory ip # Grab the IPv4 address of a running dory service 77 | dory pull # Pull down the docker images that dory uses 78 | dory restart # Stop and restart all dory services 79 | dory status # Report status of the dory services 80 | dory up # Bring up dory services (nginx-proxy, dnsmasq, resolv) 81 | dory upgrade # Upgrade dory to the latest version 82 | dory version # Check current installed version of dory 83 | 84 | Options: 85 | v, [--verbose], [--no-verbose] 86 | ``` 87 | 88 | ### Config file 89 | 90 | Dory will start looking for a config file in your current working directory, and will recurse up to `/` until it finds one. If dory does not find a config file, it will use the default settings. 91 | 92 | You can bootstrap your config file with the default settings using `dory config-file`. This 93 | file will be placed by default at `~/.dory.yml`, but again you can move it to a preferred place. This allows you to have project-specific dory configs if you so desire by putting the config at 94 | `/.dory.yml`: 95 | 96 | ```yaml 97 | --- 98 | dory: 99 | # Be careful if you change the settings of some of 100 | # these services. They may not talk to each other 101 | # if you change IP Addresses. 102 | # For example, resolv expects a nameserver listening at 103 | # the specified address. dnsmasq normally does this, 104 | # but if you disable dnsmasq, it 105 | # will make your system look for a name server that 106 | # doesn't exist. 107 | dnsmasq: 108 | enabled: true 109 | domains: # array of domains that will be resolved to the specified address 110 | - domain: docker # you can set '#' for a wildcard 111 | address: 127.0.0.1 # return for queries against the domain 112 | container_name: dory_dnsmasq 113 | port: 53 # port to listen for dns requests on. must be 53 on linux. can be anything that's open on macos 114 | # kill_others: kill processes bound to the port we need (see previous setting 'port') 115 | # Possible values: 116 | # ask (prompt about killing each time. User can accept/reject) 117 | # yes|true (go ahead and kill without asking) 118 | # no|false (don't kill, and don't even ask) 119 | kill_others: ask 120 | service_start_delay: 5 # seconds to wait after restarting systemd services 121 | nginx_proxy: 122 | enabled: true 123 | container_name: dory_dinghy_http_proxy 124 | https_enabled: true 125 | ssl_certs_dir: '' # leave as empty string to use default certs 126 | port: 80 # port 80 is default for http 127 | tls_port: 443 # port 443 is default for https 128 | 129 | resolv: 130 | enabled: true 131 | nameserver: 127.0.0.1 132 | port: 53 # port where the nameserver listens. On linux it must be 53 133 | ``` 134 | 135 | #### Upgrading existing config file 136 | 137 | If you run the `dory config-file` command and have an existing config file, 138 | dory will offer you the option of upgrading. This will preserve your settings 139 | and migrate you to the latest format. You can skip the prompt by passing the 140 | `--upgrade` flag 141 | 142 | ``` 143 | dory config-file --upgrade 144 | ``` 145 | 146 | ``` 147 | Usage: 148 | dory config-file 149 | 150 | Options: 151 | u, [--upgrade], [--no-upgrade] 152 | f, [--force] 153 | 154 | Description: 155 | Writes a dory config file to /home/ben/.dory.yml containing the default 156 | settings. This can then be configured as preferred. 157 | ``` 158 | 159 | ## Making your containers accessible by name (DNS) 160 | 161 | To make your container(s) accessible through a domain, all you have to do is set 162 | a `VIRTUAL_HOST` environment variable in your container. That's it! (Well, and you have 163 | to start dory with `dory up`) 164 | 165 | The proxy will by default use the first port exposed by your container as the HTTP port to proxy to. 166 | This can be overridden by setting the `VIRTUAL_PORT` environment variable on the container to the desired HTTP port. 167 | 168 | You will also need to set `VIRTUAL_PORT` if your server binds to something other than 80 169 | inside its container (e.g. `VIRTUAL_PORT: 3000`). This will tell the nginx proxy which 170 | port to forward traffic to in your container. When accessing the server from outside 171 | of docker, you will still hit port 80 (such as with your web browser). 172 | 173 | If your back-end container uses HTTPS, then set `VIRTUAL_PROTO: https` to tell the nginx 174 | proxy to use https instead of the default http. 175 | 176 | Many people do this in their `docker-compose.yml` file: 177 | 178 | ```yaml 179 | version: '2' 180 | services: 181 | web: 182 | build: . 183 | depends_on: 184 | - db 185 | - redis 186 | environment: 187 | VIRTUAL_HOST: myapp.docker 188 | redis: 189 | image: redis 190 | environment: 191 | VIRTUAL_HOST: redis.docker 192 | VIRTUAL_PORT: 6379 193 | db: 194 | image: postgres 195 | environment: 196 | VIRTUAL_HOST: postgres.docker 197 | VIRTUAL_PORT: 5432 198 | ``` 199 | 200 | In the example above, you can hit the web container from the host machine with `http://myapp.docker`, 201 | and the redis container with `tcp://redis.docker`. This does *not* affect any links on the internal docker network. 202 | 203 | You could also just run a docker container with the environment variable like this: 204 | 205 | ``` 206 | docker run -e VIRTUAL_HOST=myapp.docker ... 207 | ``` 208 | 209 | ## Usage with dinghy 210 | 211 | If you are using dinghy, but want to use dory to manage the proxy instead of dinghy's built-in stuff, 212 | this is now possible! (the use case for this that we ran into was multiple domain support. For example, 213 | the dev wanted to have some containers accessible at `something.docker`). To accomplish this, 214 | you need to disable dinghy's proxy stuff (otherwise dinghy and dory will stomp on each other's resolv files): 215 | 216 | In your [`~/.dinghy/preferences.yml`](https://github.com/codekitchen/dinghy#preferences) 217 | file, disable the proxy: 218 | 219 | ```yaml 220 | :preferences: 221 | :proxy_disabled: true 222 | ... 223 | ``` 224 | 225 | In your dory config file (which can be at `/.dory.yml` or anywhere else in parent directories, 226 | `~/.dory.yml` being the default) (hint: if it doesn't exist, [generate it with `dory config-file`](#config-file)), 227 | set your dnsmasq domains and their addresses to `dinghy`, as well as the resolv nameserver. Here is 228 | an example (with unrelated parts removed for ease of reading): 229 | 230 | ```yaml 231 | --- 232 | dory: 233 | dnsmasq: 234 | domains: 235 | - domain: docker 236 | address: dinghy # instead of the default 127.0.0.1 237 | ... 238 | resolv: 239 | nameserver: dinghy # instead of the default 127.0.0.1 240 | ``` 241 | 242 | If the dinghy vm gets rebooted for some reason, or otherwise changes IP addresses, 243 | you may need to restart dory to pickup the changes: 244 | 245 | ``` 246 | dory restart 247 | ``` 248 | 249 | ## Root privilege requirement 250 | 251 | To configure the local resolver, dory needs to edit the `/etc/resolv.conf`. Therefore 252 | you may be prompted for your `sudo` password during `dory up/restart/down`. 253 | If you do not want to enter your password every time you can extend the 254 | `sudoers` config as follows: 255 | 256 | ``` 257 | sudo visudo -f /etc/sudoers.d/dory-edit-resolv-conf 258 | ``` 259 | 260 | To allow passwordless execution only for a single user (replace `my-user` accordingly): 261 | 262 | ``` 263 | Cmnd_Alias DORY_EDIT_RESOLVCONF = /usr/bin/tee /etc/resolv.conf 264 | my-user ALL=(root) NOPASSWD: DORY_EDIT_RESOLVCONF 265 | ``` 266 | 267 | To allow passwordless execution for all users in group `sudo` (you can list the affected users with `awk -F':' '/sudo/{print $4}' /etc/group`): 268 | ``` 269 | Cmnd_Alias DORY_EDIT_RESOLVCONF = /usr/bin/tee /etc/resolv.conf 270 | %sudo ALL=(root) NOPASSWD: DORY_EDIT_RESOLVCONF 271 | ``` 272 | 273 | On OS X you probably need to change `%sudo` to `%admin`: 274 | ``` 275 | Cmnd_Alias DORY_EDIT_RESOLVCONF = /usr/bin/tee /etc/resolv.conf 276 | %admin ALL=(root) NOPASSWD: DORY_EDIT_RESOLVCONF 277 | ``` 278 | 279 | *Note: Changes are only applied after closing the file.* 280 | 281 | 282 | ## Troubleshooting 283 | 284 | *Help the dnsmasq container is having issues starting!* 285 | 286 | Make sure you aren't already running a dnsmasq service (or some other service) on port 53. 287 | Because the Linux resolv file doesn't have support for port numbers, we have to run 288 | on host port 53. To make matters fun, some distros (such as those shipping with 289 | [NetworkManager](https://wiki.archlinux.org/index.php/NetworkManager)) will 290 | run a dnsmasq on 53 to perform local DNS caching. This is nice, but it will 291 | conflict with Dory's dnsmasq container. You will probably want to disable it. 292 | 293 | If using Network Manager, try commenting out `dns=dnsmasq` 294 | in `/etc/NetworkManager/NetworkManager.conf`. Then restart 295 | NetworkManager: `sudo service network-manager restart` or 296 | `sudo systemctl restart NetworkManager` 297 | 298 | If you're using Network Manager/DNSMasqd to do NAT and/or share internet with the computer 299 | you are installing dory on, stop. You'd need to configure dory's built in DNSmasq to do 300 | the same, which is not trivial, out of scope, and probably more than you're bargaining for. 301 | 302 | If you are on Mac, you can choose which port to bind the dnsmasq container to. In your 303 | dory config file, adjust the setting under `dory -> dnsmasq -> port`. You probably want 304 | to make `dory -> resolv -> port` match. The default value on Mac is 19323. 305 | 306 | As of version 0.5.0, dory is a little smarter at handling this problem for you. 307 | dory will identify if you have systemd services running that will race for the port 308 | and cause issues. It will offer to put those services down temporarily and then put 309 | them back up when finished. You can configure this behavior in the [config file](#config-file) 310 | to achieve minimal annoyance (since you'll likely be prompted every time by default). 311 | 312 | ## Is this dinghy for Linux? 313 | 314 | No. Well, maybe sort of, but not really. [Dinghy](https://github.com/codekitchen/dinghy) 315 | has a lot of responsibilities on OS X, most of which are not necessary on Linux since 316 | docker runs natively. Something it does that can benefit linux users however, is the 317 | setup and management of an [nginx reverse HTTP proxy](https://www.nginx.com/resources/admin-guide/reverse-proxy/). 318 | For this reason, dory exists to provide this reverse proxy on Linux, along with 319 | accompanying dnsmasq and resolv services. 320 | Using full dinghy on Linux for local development doesn't really make sense to me, 321 | but using a reverse proxy does. Furthermore, if you work with other devs who run 322 | Dinghy on OS X, you will have to massage your [docker-compose](https://docs.docker.com/compose/) 323 | files to avoid conflicting. By using [dory](https://github.com/FreedomBen/dory), 324 | you can safely use the same `VIRTUAL_HOST` setup without conflict. And because 325 | dory uses [dinghy-http-proxy](https://github.com/codekitchen/dinghy-http-proxy) 326 | under the hood, you will be as compatible as possible. 327 | 328 | ## Are there any reasons to run full dinghy on Linux? 329 | 330 | Generally speaking, IMHO, no. The native experience is superior. However, for 331 | some reason maybe you'd prefer to not have docker on your local machine? 332 | Maybe you'd rather run it in a VM? If that describes you, then maybe you want full dinghy. 333 | 334 | I am intrigued at the possibilities of using dinghy on Linux to drive a 335 | cloud-based docker engine. For that, stay tuned. 336 | 337 | ## Why didn't you just fork dinghy? 338 | 339 | That was actually my first approach, and I considered it quite a bit. As I 340 | went through the process in my head tho, and reviewed the dinghy source code, 341 | I decided that it was just too heavy to really fit the need I had. I love being 342 | able to run docker natively, and I revere 343 | [the Arch Way](https://wiki.archlinux.org/index.php/Arch_Linux#Principles). Dinghy just 344 | seemed like too big of a hammer for this problem (the problem being that I work 345 | on Linux, but my colleagues use OS X/Dinghy for docker development). 346 | 347 | ## What if I'm developing on a cloud server? 348 | 349 | You do this too!? Well fine hacker, it's your lucky day because dory has you 350 | covered. You can run the nginx proxy on the cloud server and the dnsmasq/resolver 351 | locally. Here's how: 352 | 353 | * Install dory on both client and server: 354 | ``` 355 | gem install dory 356 | ``` 357 | * Gen a base config file: 358 | ``` 359 | dory config-file 360 | ``` 361 | * On the local machine, disable the nginx-proxy, and set the dnsmasq address to that of your cloud server: 362 | ```yaml 363 | dnsmasq: 364 | enabled: true 365 | domain: docker # domain that will be listened for 366 | address: # address returned for queries against domain 367 | container_name: dory_dnsmasq 368 | nginx_proxy: 369 | enabled: false 370 | container_name: dory_dinghy_http_proxy 371 | ``` 372 | * On the server, disable resolv and dnsmasq: 373 | ```yaml 374 | dnsmasq: 375 | enabled: false 376 | domain: docker # domain that will be listened for 377 | address: 127.0.0.1 # address returned for queries against domain 378 | container_name: dory_dnsmasq 379 | resolv: 380 | enabled: false 381 | nameserver: 127.0.0.1 382 | ``` 383 | * Profit! 384 | 385 | ## Contributing 386 | 387 | Want to contribute? Cool! Fork it, push it, request it. Please try to write tests for any functionality you add. 388 | 389 | ## Development Quick Start 390 | 391 | 1. If you want to send a pull request with your changes, then fork the repo 392 | 1. Clone it: `git clone https://github.com/FreedomBen/dory.git` or if you forked in step 1, use the URL for your fork 393 | 1. Make your changes 394 | 1. Build the gem locally: `gem build dory.gemspec` 395 | 1. Now you can run your locally built version of the gem like normal: `dory ` 396 | 1. Rinse and repeat. For easy cleaning and reinstalling, I recommend using this command, which you might want to alias: `rm *.gem; gem clean dory; yes | gem uninstall dory; gem build dory.gemspec && gem install dory*.gem` 397 | 1. Run the specs locally (note that I've attempted to make the specs interfere with the running system as minimally as possible, but some things are difficult to avoid. For example, if you have something running on port 53, the specs will kill it. Also, you will need to enter password for sudo): `bundle exec rspec spec/` 398 | 1. Specific specs can be run with: `bundle exec rspec spec/some/file.rb` 399 | 400 | ## Built on: 401 | 402 | * [jwilder/nginx-proxy](https://github.com/jwilder/nginx-proxy) (Indirectly but worthy of mention) 403 | * [codekitchen/dinghy-http-proxy](https://github.com/codekitchen/dinghy-http-proxy) 404 | * [freedomben/dory-http-proxy](https://github.com/freedomben/dory-http-proxy) 405 | * [andyshinn/dnsmasq](https://hub.docker.com/r/andyshinn/dnsmasq/) 406 | * [freedomben/dory-dnsmasq](https://github.com/FreedomBen/dory-dnsmasq) 407 | * [erikhuda/thor](https://github.com/erikhuda/thor) 408 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisabledByDefault: true 3 | 4 | #################### Lint ################################ 5 | 6 | Lint/AmbiguousOperator: 7 | Description: >- 8 | Checks for ambiguous operators in the first argument of a 9 | method invocation without parentheses. 10 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args' 11 | Enabled: true 12 | 13 | Lint/AmbiguousRegexpLiteral: 14 | Description: >- 15 | Checks for ambiguous regexp literals in the first argument of 16 | a method invocation without parenthesis. 17 | Enabled: true 18 | 19 | Lint/AssignmentInCondition: 20 | Description: "Don't use assignment in conditions." 21 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition' 22 | Enabled: true 23 | 24 | Lint/BlockAlignment: 25 | Description: 'Align block ends correctly.' 26 | Enabled: true 27 | 28 | Lint/CircularArgumentReference: 29 | Description: "Don't refer to the keyword argument in the default value." 30 | Enabled: true 31 | 32 | Lint/ConditionPosition: 33 | Description: >- 34 | Checks for condition placed in a confusing position relative to 35 | the keyword. 36 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' 37 | Enabled: true 38 | 39 | Lint/Debugger: 40 | Description: 'Check for debugger calls.' 41 | Enabled: true 42 | 43 | Lint/DefEndAlignment: 44 | Description: 'Align ends corresponding to defs correctly.' 45 | Enabled: true 46 | 47 | Lint/DeprecatedClassMethods: 48 | Description: 'Check for deprecated class method calls.' 49 | Enabled: true 50 | 51 | Lint/DuplicateMethods: 52 | Description: 'Check for duplicate methods calls.' 53 | Enabled: true 54 | 55 | Lint/EachWithObjectArgument: 56 | Description: 'Check for immutable argument given to each_with_object.' 57 | Enabled: true 58 | 59 | Lint/ElseLayout: 60 | Description: 'Check for odd code arrangement in an else block.' 61 | Enabled: true 62 | 63 | Lint/EmptyEnsure: 64 | Description: 'Checks for empty ensure block.' 65 | Enabled: true 66 | 67 | Lint/EmptyInterpolation: 68 | Description: 'Checks for empty string interpolation.' 69 | Enabled: true 70 | 71 | Lint/EndAlignment: 72 | Description: 'Align ends correctly.' 73 | Enabled: true 74 | 75 | Lint/EndInMethod: 76 | Description: 'END blocks should not be placed inside method definitions.' 77 | Enabled: true 78 | 79 | Lint/EnsureReturn: 80 | Description: 'Do not use return in an ensure block.' 81 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure' 82 | Enabled: true 83 | 84 | Lint/Eval: 85 | Description: 'The use of eval represents a serious security risk.' 86 | Enabled: true 87 | 88 | Lint/FormatParameterMismatch: 89 | Description: 'The number of parameters to format/sprint must match the fields.' 90 | Enabled: true 91 | 92 | Lint/HandleExceptions: 93 | Description: "Don't suppress exception." 94 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' 95 | Enabled: true 96 | 97 | Lint/InvalidCharacterLiteral: 98 | Description: >- 99 | Checks for invalid character literals with a non-escaped 100 | whitespace character. 101 | Enabled: true 102 | 103 | Lint/LiteralInCondition: 104 | Description: 'Checks of literals used in conditions.' 105 | Enabled: true 106 | 107 | Lint/LiteralInInterpolation: 108 | Description: 'Checks for literals used in interpolation.' 109 | Enabled: true 110 | 111 | Lint/Loop: 112 | Description: >- 113 | Use Kernel#loop with break rather than begin/end/until or 114 | begin/end/while for post-loop tests. 115 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break' 116 | Enabled: true 117 | 118 | Lint/NestedMethodDefinition: 119 | Description: 'Do not use nested method definitions.' 120 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods' 121 | Enabled: true 122 | 123 | Lint/NonLocalExitFromIterator: 124 | Description: 'Do not use return in iterator to cause non-local exit.' 125 | Enabled: true 126 | 127 | Lint/ParenthesesAsGroupedExpression: 128 | Description: >- 129 | Checks for method calls with a space before the opening 130 | parenthesis. 131 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 132 | Enabled: true 133 | 134 | Lint/RequireParentheses: 135 | Description: >- 136 | Use parentheses in the method call to avoid confusion 137 | about precedence. 138 | Enabled: true 139 | 140 | Lint/RescueException: 141 | Description: 'Avoid rescuing the Exception class.' 142 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues' 143 | Enabled: true 144 | 145 | Lint/ShadowingOuterLocalVariable: 146 | Description: >- 147 | Do not use the same name as outer local variable 148 | for block arguments or block local variables. 149 | Enabled: true 150 | 151 | Lint/StringConversionInInterpolation: 152 | Description: 'Checks for Object#to_s usage in string interpolation.' 153 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' 154 | Enabled: true 155 | 156 | Lint/UnderscorePrefixedVariableName: 157 | Description: 'Do not use prefix `_` for a variable that is used.' 158 | Enabled: true 159 | 160 | Lint/UnneededDisable: 161 | Description: >- 162 | Checks for rubocop:disable comments that can be removed. 163 | Note: this cop is not disabled when disabling all cops. 164 | It must be explicitly disabled. 165 | Enabled: true 166 | 167 | Lint/UnusedBlockArgument: 168 | Description: 'Checks for unused block arguments.' 169 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 170 | Enabled: true 171 | 172 | Lint/UnusedMethodArgument: 173 | Description: 'Checks for unused method arguments.' 174 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 175 | Enabled: true 176 | 177 | Lint/UnreachableCode: 178 | Description: 'Unreachable code.' 179 | Enabled: true 180 | 181 | Lint/UselessAccessModifier: 182 | Description: 'Checks for useless access modifiers.' 183 | Enabled: true 184 | 185 | Lint/UselessAssignment: 186 | Description: 'Checks for useless assignment to a local variable.' 187 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' 188 | Enabled: true 189 | 190 | Lint/UselessComparison: 191 | Description: 'Checks for comparison of something with itself.' 192 | Enabled: true 193 | 194 | Lint/UselessElseWithoutRescue: 195 | Description: 'Checks for useless `else` in `begin..end` without `rescue`.' 196 | Enabled: true 197 | 198 | Lint/UselessSetterCall: 199 | Description: 'Checks for useless setter call to a local variable.' 200 | Enabled: true 201 | 202 | Lint/Void: 203 | Description: 'Possible use of operator/literal/variable in void context.' 204 | Enabled: true 205 | 206 | ###################### Metrics #################################### 207 | 208 | Metrics/AbcSize: 209 | Description: >- 210 | A calculated magnitude based on number of assignments, 211 | branches, and conditions. 212 | Reference: 'http://c2.com/cgi/wiki?AbcMetric' 213 | Enabled: false 214 | Max: 20 215 | 216 | Metrics/BlockNesting: 217 | Description: 'Avoid excessive block nesting' 218 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' 219 | Enabled: true 220 | Max: 4 221 | 222 | Metrics/ClassLength: 223 | Description: 'Avoid classes longer than 250 lines of code.' 224 | Enabled: true 225 | Max: 250 226 | 227 | Metrics/CyclomaticComplexity: 228 | Description: >- 229 | A complexity metric that is strongly correlated to the number 230 | of test cases needed to validate a method. 231 | Enabled: true 232 | 233 | Metrics/LineLength: 234 | Description: 'Limit lines to 80 characters.' 235 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' 236 | Enabled: false 237 | 238 | Metrics/MethodLength: 239 | Description: 'Avoid methods longer than 30 lines of code.' 240 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' 241 | Enabled: true 242 | Max: 30 243 | 244 | Metrics/ModuleLength: 245 | Description: 'Avoid modules longer than 250 lines of code.' 246 | Enabled: true 247 | Max: 250 248 | 249 | Metrics/ParameterLists: 250 | Description: 'Avoid parameter lists longer than three or four parameters.' 251 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' 252 | Enabled: true 253 | 254 | Metrics/PerceivedComplexity: 255 | Description: >- 256 | A complexity metric geared towards measuring complexity for a 257 | human reader. 258 | Enabled: false 259 | 260 | ##################### Performance ############################# 261 | 262 | Performance/Count: 263 | Description: >- 264 | Use `count` instead of `select...size`, `reject...size`, 265 | `select...count`, `reject...count`, `select...length`, 266 | and `reject...length`. 267 | Enabled: true 268 | 269 | Performance/Detect: 270 | Description: >- 271 | Use `detect` instead of `select.first`, `find_all.first`, 272 | `select.last`, and `find_all.last`. 273 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code' 274 | Enabled: true 275 | 276 | Performance/FlatMap: 277 | Description: >- 278 | Use `Enumerable#flat_map` 279 | instead of `Enumerable#map...Array#flatten(1)` 280 | or `Enumberable#collect..Array#flatten(1)` 281 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code' 282 | Enabled: true 283 | EnabledForFlattenWithoutParams: false 284 | # If enabled, this cop will warn about usages of 285 | # `flatten` being called without any parameters. 286 | # This can be dangerous since `flat_map` will only flatten 1 level, and 287 | # `flatten` without any parameters can flatten multiple levels. 288 | 289 | Performance/ReverseEach: 290 | Description: 'Use `reverse_each` instead of `reverse.each`.' 291 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' 292 | Enabled: true 293 | 294 | Performance/Sample: 295 | Description: >- 296 | Use `sample` instead of `shuffle.first`, 297 | `shuffle.last`, and `shuffle[Fixnum]`. 298 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' 299 | Enabled: true 300 | 301 | Performance/Size: 302 | Description: >- 303 | Use `size` instead of `count` for counting 304 | the number of elements in `Array` and `Hash`. 305 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code' 306 | Enabled: true 307 | 308 | Performance/StringReplacement: 309 | Description: >- 310 | Use `tr` instead of `gsub` when you are replacing the same 311 | number of characters. Use `delete` instead of `gsub` when 312 | you are deleting characters. 313 | Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code' 314 | Enabled: true 315 | 316 | ##################### Rails ################################## 317 | 318 | Rails/ActionFilter: 319 | Description: 'Enforces consistent use of action filter methods.' 320 | Enabled: false 321 | 322 | Rails/Date: 323 | Description: >- 324 | Checks the correct usage of date aware methods, 325 | such as Date.today, Date.current etc. 326 | Enabled: false 327 | 328 | Rails/Delegate: 329 | Description: 'Prefer delegate method for delegations.' 330 | Enabled: false 331 | 332 | Rails/FindBy: 333 | Description: 'Prefer find_by over where.first.' 334 | Enabled: false 335 | 336 | Rails/FindEach: 337 | Description: 'Prefer all.find_each over all.find.' 338 | Enabled: false 339 | 340 | Rails/HasAndBelongsToMany: 341 | Description: 'Prefer has_many :through to has_and_belongs_to_many.' 342 | Enabled: false 343 | 344 | Rails/Output: 345 | Description: 'Checks for calls to puts, print, etc.' 346 | Enabled: false 347 | 348 | Rails/ReadWriteAttribute: 349 | Description: >- 350 | Checks for read_attribute(:attr) and 351 | write_attribute(:attr, val). 352 | Enabled: false 353 | 354 | Rails/ScopeArgs: 355 | Description: 'Checks the arguments of ActiveRecord scopes.' 356 | Enabled: false 357 | 358 | Rails/TimeZone: 359 | Description: 'Checks the correct usage of time zone aware methods.' 360 | StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' 361 | Reference: 'http://danilenko.org/2012/7/6/rails_timezones' 362 | Enabled: false 363 | 364 | Rails/Validation: 365 | Description: 'Use validates :attribute, hash of validations.' 366 | Enabled: false 367 | 368 | ################## Style ################################# 369 | 370 | Style/AccessModifierIndentation: 371 | Description: Check indentation of private/protected visibility modifiers. 372 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected' 373 | Enabled: false 374 | 375 | Style/AccessorMethodName: 376 | Description: Check the naming of accessor methods for get_/set_. 377 | Enabled: false 378 | 379 | Style/Alias: 380 | Description: 'Use alias_method instead of alias.' 381 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' 382 | Enabled: false 383 | 384 | Style/AlignArray: 385 | Description: >- 386 | Align the elements of an array literal if they span more than 387 | one line. 388 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' 389 | Enabled: false 390 | 391 | Style/AlignHash: 392 | Description: >- 393 | Align the elements of a hash literal if they span more than 394 | one line. 395 | Enabled: false 396 | 397 | Style/AlignParameters: 398 | Description: >- 399 | Align the parameters of a method call if they span more 400 | than one line. 401 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' 402 | Enabled: false 403 | 404 | Style/AndOr: 405 | Description: 'Use &&/|| instead of and/or.' 406 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or' 407 | Enabled: false 408 | 409 | Style/ArrayJoin: 410 | Description: 'Use Array#join instead of Array#*.' 411 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' 412 | Enabled: false 413 | 414 | Style/AsciiComments: 415 | Description: 'Use only ascii symbols in comments.' 416 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' 417 | Enabled: false 418 | 419 | Style/AsciiIdentifiers: 420 | Description: 'Use only ascii symbols in identifiers.' 421 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' 422 | Enabled: false 423 | 424 | Style/Attr: 425 | Description: 'Checks for uses of Module#attr.' 426 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' 427 | Enabled: false 428 | 429 | Style/BeginBlock: 430 | Description: 'Avoid the use of BEGIN blocks.' 431 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks' 432 | Enabled: false 433 | 434 | Style/BarePercentLiterals: 435 | Description: 'Checks if usage of %() or %Q() matches configuration.' 436 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand' 437 | Enabled: false 438 | 439 | Style/BlockComments: 440 | Description: 'Do not use block comments.' 441 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments' 442 | Enabled: false 443 | 444 | Style/BlockEndNewline: 445 | Description: 'Put end statement of multiline block on its own line.' 446 | Enabled: false 447 | 448 | Style/BlockDelimiters: 449 | Description: >- 450 | Avoid using {...} for multi-line blocks (multiline chaining is 451 | always ugly). 452 | Prefer {...} over do...end for single-line blocks. 453 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 454 | Enabled: false 455 | 456 | Style/BracesAroundHashParameters: 457 | Description: 'Enforce braces style around hash parameters.' 458 | Enabled: false 459 | 460 | Style/CaseEquality: 461 | Description: 'Avoid explicit use of the case equality operator(===).' 462 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' 463 | Enabled: false 464 | 465 | Style/CaseIndentation: 466 | Description: 'Indentation of when in a case/when/[else/]end.' 467 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case' 468 | Enabled: false 469 | 470 | Style/CharacterLiteral: 471 | Description: 'Checks for uses of character literals.' 472 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals' 473 | Enabled: false 474 | 475 | Style/ClassAndModuleCamelCase: 476 | Description: 'Use CamelCase for classes and modules.' 477 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes' 478 | Enabled: false 479 | 480 | Style/ClassAndModuleChildren: 481 | Description: 'Checks style of children classes and modules.' 482 | Enabled: false 483 | 484 | Style/ClassCheck: 485 | Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.' 486 | Enabled: false 487 | 488 | Style/ClassMethods: 489 | Description: 'Use self when defining module/class methods.' 490 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-class-methods' 491 | Enabled: false 492 | 493 | Style/ClassVars: 494 | Description: 'Avoid the use of class variables.' 495 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars' 496 | Enabled: false 497 | 498 | Style/ClosingParenthesisIndentation: 499 | Description: 'Checks the indentation of hanging closing parentheses.' 500 | Enabled: false 501 | 502 | Style/ColonMethodCall: 503 | Description: 'Do not use :: for method call.' 504 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' 505 | Enabled: false 506 | 507 | Style/CommandLiteral: 508 | Description: 'Use `` or %x around command literals.' 509 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' 510 | Enabled: false 511 | 512 | Style/CommentAnnotation: 513 | Description: 'Checks formatting of annotation comments.' 514 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' 515 | Enabled: false 516 | 517 | Style/CommentIndentation: 518 | Description: 'Indentation of comments.' 519 | Enabled: false 520 | 521 | Style/ConstantName: 522 | Description: 'Constants should use SCREAMING_SNAKE_CASE.' 523 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case' 524 | Enabled: false 525 | 526 | Style/DefWithParentheses: 527 | Description: 'Use def with parentheses when there are arguments.' 528 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 529 | Enabled: false 530 | 531 | Style/Documentation: 532 | Description: 'Document classes and non-namespace modules.' 533 | Enabled: false 534 | 535 | Style/DotPosition: 536 | Description: 'Checks the position of the dot in multi-line method calls.' 537 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' 538 | Enabled: false 539 | 540 | Style/DoubleNegation: 541 | Description: 'Checks for uses of double negation (!!).' 542 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' 543 | Enabled: false 544 | 545 | Style/EachWithObject: 546 | Description: 'Prefer `each_with_object` over `inject` or `reduce`.' 547 | Enabled: false 548 | 549 | Style/ElseAlignment: 550 | Description: 'Align elses and elsifs correctly.' 551 | Enabled: false 552 | 553 | Style/EmptyElse: 554 | Description: 'Avoid empty else-clauses.' 555 | Enabled: false 556 | 557 | Style/EmptyLineBetweenDefs: 558 | Description: 'Use empty lines between defs.' 559 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods' 560 | Enabled: false 561 | 562 | Style/EmptyLines: 563 | Description: "Don't use several empty lines in a row." 564 | Enabled: false 565 | 566 | Style/EmptyLinesAroundAccessModifier: 567 | Description: "Keep blank lines around access modifiers." 568 | Enabled: false 569 | 570 | Style/EmptyLinesAroundBlockBody: 571 | Description: "Keeps track of empty lines around block bodies." 572 | Enabled: false 573 | 574 | Style/EmptyLinesAroundClassBody: 575 | Description: "Keeps track of empty lines around class bodies." 576 | Enabled: false 577 | 578 | Style/EmptyLinesAroundModuleBody: 579 | Description: "Keeps track of empty lines around module bodies." 580 | Enabled: false 581 | 582 | Style/EmptyLinesAroundMethodBody: 583 | Description: "Keeps track of empty lines around method bodies." 584 | Enabled: false 585 | 586 | Style/EmptyLiteral: 587 | Description: 'Prefer literals to Array.new/Hash.new/String.new.' 588 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' 589 | Enabled: false 590 | 591 | Style/EndBlock: 592 | Description: 'Avoid the use of END blocks.' 593 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' 594 | Enabled: false 595 | 596 | Style/EndOfLine: 597 | Description: 'Use Unix-style line endings.' 598 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' 599 | Enabled: false 600 | 601 | Style/EvenOdd: 602 | Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' 603 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 604 | Enabled: false 605 | 606 | Style/ExtraSpacing: 607 | Description: 'Do not use unnecessary spacing.' 608 | Enabled: false 609 | 610 | Style/FileName: 611 | Description: 'Use snake_case for source file names.' 612 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' 613 | Enabled: false 614 | 615 | Style/InitialIndentation: 616 | Description: >- 617 | Checks the indentation of the first non-blank non-comment line in a file. 618 | Enabled: false 619 | 620 | Style/FirstParameterIndentation: 621 | Description: 'Checks the indentation of the first parameter in a method call.' 622 | Enabled: false 623 | 624 | Style/FlipFlop: 625 | Description: 'Checks for flip flops' 626 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' 627 | Enabled: false 628 | 629 | Style/For: 630 | Description: 'Checks use of for or each in multiline loops.' 631 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops' 632 | Enabled: false 633 | 634 | Style/FormatString: 635 | Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' 636 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' 637 | Enabled: false 638 | 639 | Style/GlobalVars: 640 | Description: 'Do not introduce global variables.' 641 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars' 642 | Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html' 643 | Enabled: false 644 | 645 | Style/GuardClause: 646 | Description: 'Check for conditionals that can be replaced with guard clauses' 647 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 648 | Enabled: false 649 | 650 | Style/HashSyntax: 651 | Description: >- 652 | Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax 653 | { :a => 1, :b => 2 }. 654 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals' 655 | Enabled: false 656 | 657 | Style/IfUnlessModifier: 658 | Description: >- 659 | Favor modifier if/unless usage when you have a 660 | single-line body. 661 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' 662 | Enabled: false 663 | 664 | Style/IfWithSemicolon: 665 | Description: 'Do not use if x; .... Use the ternary operator instead.' 666 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' 667 | Enabled: false 668 | 669 | Style/IndentationConsistency: 670 | Description: 'Keep indentation straight.' 671 | Enabled: false 672 | 673 | Style/IndentationWidth: 674 | Description: 'Use 2 spaces for indentation.' 675 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 676 | Enabled: false 677 | 678 | Style/IndentArray: 679 | Description: >- 680 | Checks the indentation of the first element in an array 681 | literal. 682 | Enabled: false 683 | 684 | Style/IndentHash: 685 | Description: 'Checks the indentation of the first key in a hash literal.' 686 | Enabled: false 687 | 688 | Style/InfiniteLoop: 689 | Description: 'Use Kernel#loop for infinite loops.' 690 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop' 691 | Enabled: false 692 | 693 | Style/Lambda: 694 | Description: 'Use the new lambda literal syntax for single-line blocks.' 695 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line' 696 | Enabled: false 697 | 698 | Style/LambdaCall: 699 | Description: 'Use lambda.call(...) instead of lambda.(...).' 700 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call' 701 | Enabled: false 702 | 703 | Style/LeadingCommentSpace: 704 | Description: 'Comments should start with a space.' 705 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' 706 | Enabled: false 707 | 708 | Style/LineEndConcatenation: 709 | Description: >- 710 | Use \ instead of + or << to concatenate two string literals at 711 | line end. 712 | Enabled: false 713 | 714 | Style/MethodCallWithoutArgsParentheses: 715 | Description: 'Do not use parentheses for method calls with no arguments.' 716 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' 717 | Enabled: false 718 | 719 | Style/MethodDefParentheses: 720 | Description: >- 721 | Checks if the method definitions have or don't have 722 | parentheses. 723 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' 724 | Enabled: false 725 | 726 | Style/MethodName: 727 | Description: 'Use the configured style when naming methods.' 728 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 729 | Enabled: false 730 | 731 | Style/ModuleFunction: 732 | Description: 'Checks for usage of `extend self` in modules.' 733 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function' 734 | Enabled: false 735 | 736 | Style/MultilineBlockChain: 737 | Description: 'Avoid multi-line chains of blocks.' 738 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 739 | Enabled: false 740 | 741 | Style/MultilineBlockLayout: 742 | Description: 'Ensures newlines after multiline block do statements.' 743 | Enabled: false 744 | 745 | Style/MultilineIfThen: 746 | Description: 'Do not use then for multi-line if/unless.' 747 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then' 748 | Enabled: false 749 | 750 | Style/MultilineOperationIndentation: 751 | Description: >- 752 | Checks indentation of binary operations that span more than 753 | one line. 754 | Enabled: false 755 | 756 | Style/MultilineTernaryOperator: 757 | Description: >- 758 | Avoid multi-line ?: (the ternary operator); 759 | use if/unless instead. 760 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary' 761 | Enabled: false 762 | 763 | Style/NegatedIf: 764 | Description: >- 765 | Favor unless over if for negative conditions 766 | (or control flow or). 767 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' 768 | Enabled: false 769 | 770 | Style/NegatedWhile: 771 | Description: 'Favor until over while for negative conditions.' 772 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives' 773 | Enabled: false 774 | 775 | Style/NestedTernaryOperator: 776 | Description: 'Use one expression per branch in a ternary operator.' 777 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary' 778 | Enabled: false 779 | 780 | Style/Next: 781 | Description: 'Use `next` to skip iteration instead of a condition at the end.' 782 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' 783 | Enabled: false 784 | 785 | Style/NilComparison: 786 | Description: 'Prefer x.nil? to x == nil.' 787 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' 788 | Enabled: false 789 | 790 | Style/NonNilCheck: 791 | Description: 'Checks for redundant nil checks.' 792 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks' 793 | Enabled: false 794 | 795 | Style/Not: 796 | Description: 'Use ! instead of not.' 797 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' 798 | Enabled: false 799 | 800 | Style/NumericLiterals: 801 | Description: >- 802 | Add underscores to large numeric literals to improve their 803 | readability. 804 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' 805 | Enabled: false 806 | 807 | Style/OneLineConditional: 808 | Description: >- 809 | Favor the ternary operator(?:) over 810 | if/then/else/end constructs. 811 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' 812 | Enabled: false 813 | 814 | Style/OpMethod: 815 | Description: 'When defining binary operators, name the argument other.' 816 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' 817 | Enabled: false 818 | 819 | Style/OptionalArguments: 820 | Description: >- 821 | Checks for optional arguments that do not appear at the end 822 | of the argument list 823 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#optional-arguments' 824 | Enabled: false 825 | 826 | Style/ParallelAssignment: 827 | Description: >- 828 | Check for simple usages of parallel assignment. 829 | It will only warn when the number of variables 830 | matches on both sides of the assignment. 831 | This also provides performance benefits 832 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment' 833 | Enabled: false 834 | 835 | Style/ParenthesesAroundCondition: 836 | Description: >- 837 | Don't use parentheses around the condition of an 838 | if/unless/while. 839 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if' 840 | Enabled: false 841 | 842 | Style/PercentLiteralDelimiters: 843 | Description: 'Use `%`-literal delimiters consistently' 844 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' 845 | Enabled: false 846 | 847 | Style/PercentQLiterals: 848 | Description: 'Checks if uses of %Q/%q match the configured preference.' 849 | Enabled: false 850 | 851 | Style/PerlBackrefs: 852 | Description: 'Avoid Perl-style regex back references.' 853 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' 854 | Enabled: false 855 | 856 | Style/PredicateName: 857 | Description: 'Check the names of predicate methods.' 858 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' 859 | Enabled: false 860 | 861 | Style/Proc: 862 | Description: 'Use proc instead of Proc.new.' 863 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' 864 | Enabled: false 865 | 866 | Style/RaiseArgs: 867 | Description: 'Checks the arguments passed to raise/fail.' 868 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages' 869 | Enabled: false 870 | 871 | Style/RedundantBegin: 872 | Description: "Don't use begin blocks when they are not needed." 873 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit' 874 | Enabled: false 875 | 876 | Style/RedundantException: 877 | Description: "Checks for an obsolete RuntimeException argument in raise/fail." 878 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror' 879 | Enabled: false 880 | 881 | Style/RedundantReturn: 882 | Description: "Don't use return where it's not required." 883 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' 884 | Enabled: false 885 | 886 | Style/RedundantSelf: 887 | Description: "Don't use self where it's not needed." 888 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required' 889 | Enabled: false 890 | 891 | Style/RegexpLiteral: 892 | Description: 'Use / or %r around regular expressions.' 893 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' 894 | Enabled: false 895 | 896 | Style/RescueEnsureAlignment: 897 | Description: 'Align rescues and ensures correctly.' 898 | Enabled: false 899 | 900 | Style/RescueModifier: 901 | Description: 'Avoid using rescue in its modifier form.' 902 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers' 903 | Enabled: false 904 | 905 | Style/SelfAssignment: 906 | Description: >- 907 | Checks for places where self-assignment shorthand should have 908 | been used. 909 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment' 910 | Enabled: false 911 | 912 | Style/Semicolon: 913 | Description: "Don't use semicolons to terminate expressions." 914 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon' 915 | Enabled: false 916 | 917 | Style/SignalException: 918 | Description: 'Checks for proper usage of fail and raise.' 919 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method' 920 | Enabled: false 921 | 922 | Style/SingleLineBlockParams: 923 | Description: 'Enforces the names of some block params.' 924 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' 925 | Enabled: false 926 | 927 | Style/SingleLineMethods: 928 | Description: 'Avoid single-line methods.' 929 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' 930 | Enabled: false 931 | 932 | Style/SpaceBeforeFirstArg: 933 | Description: >- 934 | Checks that exactly one space is used between a method name 935 | and the first argument for method calls without parentheses. 936 | Enabled: true 937 | 938 | Style/SpaceAfterColon: 939 | Description: 'Use spaces after colons.' 940 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 941 | Enabled: false 942 | 943 | Style/SpaceAfterComma: 944 | Description: 'Use spaces after commas.' 945 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 946 | Enabled: false 947 | 948 | Style/SpaceAroundKeyword: 949 | Description: 'Use spaces around keywords.' 950 | Enabled: false 951 | 952 | Style/SpaceAfterMethodName: 953 | Description: >- 954 | Do not put a space between a method name and the opening 955 | parenthesis in a method definition. 956 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' 957 | Enabled: false 958 | 959 | Style/SpaceAfterNot: 960 | Description: Tracks redundant space after the ! operator. 961 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang' 962 | Enabled: false 963 | 964 | Style/SpaceAfterSemicolon: 965 | Description: 'Use spaces after semicolons.' 966 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 967 | Enabled: false 968 | 969 | Style/SpaceBeforeBlockBraces: 970 | Description: >- 971 | Checks that the left block brace has or doesn't have space 972 | before it. 973 | Enabled: false 974 | 975 | Style/SpaceBeforeComma: 976 | Description: 'No spaces before commas.' 977 | Enabled: false 978 | 979 | Style/SpaceBeforeComment: 980 | Description: >- 981 | Checks for missing space between code and a comment on the 982 | same line. 983 | Enabled: false 984 | 985 | Style/SpaceBeforeSemicolon: 986 | Description: 'No spaces before semicolons.' 987 | Enabled: false 988 | 989 | Style/SpaceInsideBlockBraces: 990 | Description: >- 991 | Checks that block braces have or don't have surrounding space. 992 | For blocks taking parameters, checks that the left brace has 993 | or doesn't have trailing space. 994 | Enabled: false 995 | 996 | Style/SpaceAroundBlockParameters: 997 | Description: 'Checks the spacing inside and after block parameters pipes.' 998 | Enabled: false 999 | 1000 | Style/SpaceAroundEqualsInParameterDefault: 1001 | Description: >- 1002 | Checks that the equals signs in parameter default assignments 1003 | have or don't have surrounding space depending on 1004 | configuration. 1005 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals' 1006 | Enabled: false 1007 | 1008 | Style/SpaceAroundOperators: 1009 | Description: 'Use a single space around operators.' 1010 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1011 | Enabled: false 1012 | 1013 | Style/SpaceInsideBrackets: 1014 | Description: 'No spaces after [ or before ].' 1015 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' 1016 | Enabled: false 1017 | 1018 | Style/SpaceInsideHashLiteralBraces: 1019 | Description: "Use spaces inside hash literal braces - or don't." 1020 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' 1021 | Enabled: false 1022 | 1023 | Style/SpaceInsideParens: 1024 | Description: 'No spaces after ( or before ).' 1025 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' 1026 | Enabled: false 1027 | 1028 | Style/SpaceInsideRangeLiteral: 1029 | Description: 'No spaces inside range literals.' 1030 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals' 1031 | Enabled: false 1032 | 1033 | Style/SpaceInsideStringInterpolation: 1034 | Description: 'Checks for padding/surrounding spaces inside string interpolation.' 1035 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#string-interpolation' 1036 | Enabled: false 1037 | 1038 | Style/SpecialGlobalVars: 1039 | Description: 'Avoid Perl-style global variables.' 1040 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' 1041 | Enabled: false 1042 | 1043 | Style/StringLiterals: 1044 | Description: 'Checks if uses of quotes match the configured preference.' 1045 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals' 1046 | Enabled: false 1047 | 1048 | Style/StringLiteralsInInterpolation: 1049 | Description: >- 1050 | Checks if uses of quotes inside expressions in interpolated 1051 | strings match the configured preference. 1052 | Enabled: false 1053 | 1054 | Style/StructInheritance: 1055 | Description: 'Checks for inheritance from Struct.new.' 1056 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new' 1057 | Enabled: false 1058 | 1059 | Style/SymbolLiteral: 1060 | Description: 'Use plain symbols instead of string symbols when possible.' 1061 | Enabled: false 1062 | 1063 | Style/SymbolProc: 1064 | Description: 'Use symbols as procs instead of blocks when possible.' 1065 | Enabled: false 1066 | 1067 | Style/Tab: 1068 | Description: 'No hard tabs.' 1069 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' 1070 | Enabled: false 1071 | 1072 | Style/TrailingBlankLines: 1073 | Description: 'Checks trailing blank lines and final newline.' 1074 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' 1075 | Enabled: false 1076 | 1077 | Style/TrailingCommaInArguments: 1078 | Description: 'Checks for trailing comma in parameter lists.' 1079 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma' 1080 | Enabled: false 1081 | 1082 | Style/TrailingCommaInLiteral: 1083 | Description: 'Checks for trailing comma in literals.' 1084 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 1085 | Enabled: false 1086 | 1087 | Style/TrailingWhitespace: 1088 | Description: 'Avoid trailing whitespace.' 1089 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' 1090 | Enabled: false 1091 | 1092 | Style/TrivialAccessors: 1093 | Description: 'Prefer attr_* methods to trivial readers/writers.' 1094 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' 1095 | Enabled: false 1096 | 1097 | Style/UnlessElse: 1098 | Description: >- 1099 | Do not use unless with else. Rewrite these with the positive 1100 | case first. 1101 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' 1102 | Enabled: false 1103 | 1104 | Style/UnneededCapitalW: 1105 | Description: 'Checks for %W when interpolation is not needed.' 1106 | Enabled: false 1107 | 1108 | Style/UnneededPercentQ: 1109 | Description: 'Checks for %q/%Q when single quotes or double quotes would do.' 1110 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' 1111 | Enabled: false 1112 | 1113 | Style/TrailingUnderscoreVariable: 1114 | Description: >- 1115 | Checks for the usage of unneeded trailing underscores at the 1116 | end of parallel variable assignment. 1117 | Enabled: false 1118 | 1119 | Style/VariableInterpolation: 1120 | Description: >- 1121 | Don't interpolate global, instance and class variables 1122 | directly in strings. 1123 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate' 1124 | Enabled: false 1125 | 1126 | Style/VariableName: 1127 | Description: 'Use the configured style when naming variables.' 1128 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' 1129 | Enabled: false 1130 | 1131 | Style/WhenThen: 1132 | Description: 'Use when x then ... for one-line cases.' 1133 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases' 1134 | Enabled: false 1135 | 1136 | Style/WhileUntilDo: 1137 | Description: 'Checks for redundant do after while or until.' 1138 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do' 1139 | Enabled: false 1140 | 1141 | Style/WhileUntilModifier: 1142 | Description: >- 1143 | Favor modifier while/until usage when you have a 1144 | single-line body. 1145 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' 1146 | Enabled: false 1147 | 1148 | Style/WordArray: 1149 | Description: 'Use %w or %W for arrays of words.' 1150 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' 1151 | Enabled: false 1152 | --------------------------------------------------------------------------------