diff --git a/CHANGELOG.md b/CHANGELOG.md index 321f789a..cd6c2408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Reverse Chronological Order: https://github.com/capistrano/capistrano/compare/v3.5.0...HEAD + * Added a `doctor:servers` subtask that outputs a summary of servers, roles & properties (@irvingwashington) * Raise a better error when an ‘after’ hook isn’t found (@jdelStrother) * Restrict the uploaded git wrapper script permissions to 700 (@irvingwashington) * Make path to git wrapper script configurable (@thickpaddy) diff --git a/lib/capistrano/configuration.rb b/lib/capistrano/configuration.rb index fef90938..b88ce537 100644 --- a/lib/capistrano/configuration.rb +++ b/lib/capistrano/configuration.rb @@ -133,16 +133,16 @@ module Capistrano installer.install(plugin, load_hooks: load_hooks) end + def servers + @servers ||= Servers.new + end + private def cmdline_filters @cmdline_filters ||= [] end - def servers - @servers ||= Servers.new - end - def installer @installer ||= PluginInstaller.new end diff --git a/lib/capistrano/configuration/server.rb b/lib/capistrano/configuration/server.rb index ca5f1547..bf7da8df 100644 --- a/lib/capistrano/configuration/server.rb +++ b/lib/capistrano/configuration/server.rb @@ -119,6 +119,10 @@ module Capistrano end end + def to_h + @properties + end + private def lvalue(key) diff --git a/lib/capistrano/doctor.rb b/lib/capistrano/doctor.rb index f38af290..4fa42398 100644 --- a/lib/capistrano/doctor.rb +++ b/lib/capistrano/doctor.rb @@ -1,5 +1,6 @@ require "capistrano/doctor/environment_doctor" require "capistrano/doctor/gems_doctor" require "capistrano/doctor/variables_doctor" +require "capistrano/doctor/servers_doctor" load File.expand_path("../tasks/doctor.rake", __FILE__) diff --git a/lib/capistrano/doctor/servers_doctor.rb b/lib/capistrano/doctor/servers_doctor.rb new file mode 100644 index 00000000..645c18c1 --- /dev/null +++ b/lib/capistrano/doctor/servers_doctor.rb @@ -0,0 +1,105 @@ +require "capistrano/doctor/output_helpers" + +module Capistrano + module Doctor + class ServersDoctor + include Capistrano::Doctor::OutputHelpers + + def initialize(env=Capistrano::Configuration.env) + @servers = env.servers.to_a + end + + def call + title("Servers (#{servers.size})") + rwc = RoleWhitespaceChecker.new(servers) + + table(servers) do |server, row| + sd = ServerDecorator.new(server) + + row << sd.uri_form + row << sd.roles + row << sd.properties + row.yellow if rwc.any_has_whitespace?(server.roles) + end + + if rwc.whitespace_roles.any? + warning "\nWhitespace detected in role(s) #{rwc.whitespace_roles_decorated}. " \ + "This might be a result of a mistyped \"%w()\" array literal." + end + puts + end + + private + + attr_reader :servers + + class RoleWhitespaceChecker + attr_reader :whitespace_roles, :servers + + def initialize(servers) + @servers = servers + @whitespace_roles = find_whitespace_roles + end + + def any_has_whitespace?(roles) + roles.any? { |role| include_whitespace?(role) } + end + + def include_whitespace?(role) + role =~ /\s/ + end + + def whitespace_roles_decorated + whitespace_roles.map(&:inspect).join(", ") + end + + private + + def find_whitespace_roles + servers.map(&:roles).map(&:to_a).flatten.uniq + .select { |role| include_whitespace?(role) } + end + end + + class ServerDecorator + def initialize(server) + @server = server + end + + def uri_form + [ + server.user, + server.user && "@", + server.hostname, + server.port && ":", + server.port + ].compact.join + end + + def roles + server.roles.to_a.inspect + end + + def properties + return "" unless server.properties.keys.any? + pretty_inspect(server.properties.to_h) + end + + private + + attr_reader :server + + # Hashes with proper padding + def pretty_inspect(element) + return element.inspect unless element.is_a?(Hash) + + pairs_string = element.keys.map do |key| + [pretty_inspect(key), pretty_inspect(element.fetch(key))].join(" => ") + end.join(", ") + + "{ #{pairs_string} }" + end + end + end + end +end diff --git a/lib/capistrano/tasks/doctor.rake b/lib/capistrano/tasks/doctor.rake index dbddff48..e588a2e9 100644 --- a/lib/capistrano/tasks/doctor.rake +++ b/lib/capistrano/tasks/doctor.rake @@ -1,5 +1,5 @@ desc "Display a Capistrano troubleshooting report (all doctor: tasks)" -task doctor: ["doctor:environment", "doctor:gems", "doctor:variables"] +task doctor: ["doctor:environment", "doctor:gems", "doctor:variables", "doctor:servers"] namespace :doctor do desc "Display Ruby environment details" @@ -16,4 +16,9 @@ namespace :doctor do task :variables do Capistrano::Doctor::VariablesDoctor.new.call end + + desc "Display the effective servers configuration" + task :servers do + Capistrano::Doctor::ServersDoctor.new.call + end end diff --git a/spec/lib/capistrano/doctor/servers_doctor_spec.rb b/spec/lib/capistrano/doctor/servers_doctor_spec.rb new file mode 100644 index 00000000..474ce0e0 --- /dev/null +++ b/spec/lib/capistrano/doctor/servers_doctor_spec.rb @@ -0,0 +1,85 @@ +require "spec_helper" +require "capistrano/doctor/servers_doctor" + +module Capistrano + module Doctor + describe ServersDoctor do + include Capistrano::DSL + let(:doc) { ServersDoctor.new } + + after { Capistrano::Configuration.reset! } + + it "prints using 4-space indentation" do + expect { doc.call }.to output(/^ {4}/).to_stdout + end + + it "prints the number of defined servers" do + role :app, %w(example.com) + server "www@example.com:22" + + expect { doc.call }.to output(/Servers \(2\)/).to_stdout + end + + describe "prints the server's details" do + it "including username" do + server "www@example.com" + expect { doc.call }.to output(/www@example.com/).to_stdout + end + + it "including port" do + server "www@example.com:22" + expect { doc.call }.to output(/www@example.com:22/).to_stdout + end + + it "including roles" do + role :app, %w(example.com) + expect { doc.call }.to output(/example.com\s+\[:app\]/).to_stdout + end + + it "including empty roles" do + server "example.com" + expect { doc.call }.to output(/example.com\s+\[\]/).to_stdout + end + + it "including properties" do + server "example.com", roles: %w(app db), primary: true + expect { doc.call }.to \ + output(/example.com\s+\[:app, :db\]\s+\{ :primary => true \}/).to_stdout + end + + it "including misleading role name alert" do + server "example.com", roles: ["web app db"] + warning_msg = 'Whitespace detected in role(s) :"web app db". ' \ + 'This might be a result of a mistyped "%w()" array literal' + + expect { doc.call }.to output(/#{Regexp.escape(warning_msg)}/).to_stdout + end + end + + it "doesn't fail for no servers" do + expect { doc.call }.to output("\nServers (0)\n \n").to_stdout + end + + describe "Rake" do + before do + load File.expand_path("../../../../../lib/capistrano/doctor.rb", + __FILE__) + end + + after do + Rake::Task.clear + end + + it "has an doctor:servers task that calls ServersDoctor" do + ServersDoctor.any_instance.expects(:call) + Rake::Task["doctor:servers"].invoke + end + + it "has a doctor task that depends on doctor:servers" do + expect(Rake::Task["doctor"].prerequisites).to \ + include("doctor:servers") + end + end + end + end +end