diff options
Diffstat (limited to 'railties/lib/rails/commands')
38 files changed, 1506 insertions, 770 deletions
diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb deleted file mode 100644 index c998e6b6a8..0000000000 --- a/railties/lib/rails/commands/application.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rails/generators' -require 'rails/generators/rails/app/app_generator' - -module Rails - module Generators - class AppGenerator # :nodoc: - # We want to exit on failure to be kind to other libraries - # This is only when accessing via CLI - def self.exit_on_failure? - true - end - end - end -end - -args = Rails::Generators::ARGVScrubber.new(ARGV).prepare! -Rails::Generators::AppGenerator.start args diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb new file mode 100644 index 0000000000..f77553b830 --- /dev/null +++ b/railties/lib/rails/commands/application/application_command.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/rails/app/app_generator" + +module Rails + module Generators + class AppGenerator # :nodoc: + # We want to exit on failure to be kind to other libraries + # This is only when accessing via CLI + def self.exit_on_failure? + true + end + end + end + + module Command + class ApplicationCommand < Base # :nodoc: + hide_command! + + def help + perform # Punt help output to the generator. + end + + def perform(*args) + Rails::Generators::AppGenerator.start \ + Rails::Generators::ARGVScrubber.new(args).prepare! + end + end + end +end diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb deleted file mode 100644 index da3b9452d5..0000000000 --- a/railties/lib/rails/commands/commands_tasks.rb +++ /dev/null @@ -1,180 +0,0 @@ -require 'rails/commands/rake_proxy' - -module Rails - # This is a class which takes in a rails command and initiates the appropriate - # initiation sequence. - # - # Warning: This class mutates ARGV because some commands require manipulating - # it before they are run. - class CommandsTasks # :nodoc: - include Rails::RakeProxy - - attr_reader :argv - - HELP_MESSAGE = <<-EOT -Usage: rails COMMAND [ARGS] - -The most common rails commands are: - generate Generate new code (short-cut alias: "g") - console Start the Rails console (short-cut alias: "c") - server Start the Rails server (short-cut alias: "s") - test Run tests (short-cut alias: "t") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" - -All commands can be run with -h (or --help) for more information. - -In addition to those commands, there are: -EOT - - ADDITIONAL_COMMANDS = [ - [ 'destroy', 'Undo code generated with "generate" (short-cut alias: "d")' ], - [ 'plugin new', 'Generates skeleton for developing a Rails plugin' ], - [ 'runner', - 'Run a piece of code in the application environment (short-cut alias: "r")' ] - ] - - COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test) - - def initialize(argv) - @argv = argv - end - - def run_command!(command) - command = parse_command(command) - - if COMMAND_WHITELIST.include?(command) - send(command) - else - run_rake_task(command) - end - end - - def plugin - require_command!("plugin") - end - - def generate - generate_or_destroy(:generate) - end - - def destroy - generate_or_destroy(:destroy) - end - - def console - require_command!("console") - options = Rails::Console.parse_arguments(argv) - - # RAILS_ENV needs to be set before config/application is required - ENV['RAILS_ENV'] = options[:environment] if options[:environment] - - # shift ARGV so IRB doesn't freak - shift_argv! - - require_application_and_environment! - Rails::Console.start(Rails.application, options) - end - - def server - set_application_directory! - require_command!("server") - - Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. - require APP_PATH - Dir.chdir(Rails.application.root) - server.start - end - end - - def test - require_command!("test") - end - - def dbconsole - require_command!("dbconsole") - Rails::DBConsole.start - end - - def runner - require_command!("runner") - end - - def new - if %w(-h --help).include?(argv.first) - require_command!("application") - else - exit_with_initialization_warning! - end - end - - def version - argv.unshift '--version' - require_command!("application") - end - - def help - write_help_message - write_commands ADDITIONAL_COMMANDS + formatted_rake_tasks - end - - private - - def exit_with_initialization_warning! - puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" - puts "Type 'rails' for help." - exit(1) - end - - def shift_argv! - argv.shift if argv.first && argv.first[0] != '-' - end - - def require_command!(command) - require "rails/commands/#{command}" - end - - def generate_or_destroy(command) - require 'rails/generators' - require_application_and_environment! - Rails.application.load_generators - require_command!(command) - end - - # Change to the application's path if there is no config.ru file in current directory. - # This allows us to run `rails server` from other directories, but still get - # the main config.ru and properly set the tmp directory. - def set_application_directory! - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru")) - end - - def require_application_and_environment! - require APP_PATH - Rails.application.require_environment! - end - - def write_help_message - puts HELP_MESSAGE - end - - def write_commands(commands) - width = commands.map { |name, _| name.size }.max || 10 - commands.each { |command| printf(" %-#{width}s %s\n", *command) } - end - - def parse_command(command) - case command - when '--version', '-v' - 'version' - when '--help', '-h' - 'help' - else - command - end - end - end -end diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb deleted file mode 100644 index ea5d20ea24..0000000000 --- a/railties/lib/rails/commands/console.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'optparse' -require 'irb' -require 'irb/completion' -require 'rails/commands/console_helper' - -module Rails - class Console - include ConsoleHelper - - class << self - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails console [environment] [options]" - opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v } - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development") { |v| options[:environment] = v.strip } - opt.parse!(arguments) - end - - set_options_env(arguments, options) - end - end - - attr_reader :options, :app, :console - - def initialize(app, options={}) - @app = app - @options = options - - app.sandbox = sandbox? - app.load_console - - @console = app.config.console || IRB - end - - def sandbox? - options[:sandbox] - end - - def environment - options[:environment] ||= super - end - alias_method :environment?, :environment - - def set_environment! - Rails.env = environment - end - - def start - set_environment! if environment? - - if sandbox? - puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})" - puts "Any modifications you make will be rolled back on exit" - else - puts "Loading #{Rails.env} environment (Rails #{Rails.version})" - end - - if defined?(console::ExtendCommandBundle) - console::ExtendCommandBundle.include(Rails::ConsoleMethods) - end - console.start - end - end -end diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb new file mode 100644 index 0000000000..7a9eaefea1 --- /dev/null +++ b/railties/lib/rails/commands/console/console_command.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "irb" +require "irb/completion" + +require "rails/command/environment_argument" + +module Rails + class Console + module BacktraceCleaner + def filter_backtrace(bt) + if result = super + Rails.backtrace_cleaner.filter([result]).first + end + end + end + + def self.start(*args) + new(*args).start + end + + attr_reader :options, :app, :console + + def initialize(app, options = {}) + @app = app + @options = options + + app.sandbox = sandbox? + + if sandbox? && app.config.disable_sandbox + puts "Error: Unable to start console in sandbox mode as sandbox mode is disabled (config.disable_sandbox is true)." + exit 1 + end + + app.load_console + + @console = app.config.console || IRB + + if @console == IRB + IRB::WorkSpace.prepend(BacktraceCleaner) + end + end + + def sandbox? + options[:sandbox] + end + + def environment + options[:environment] + end + alias_method :environment?, :environment + + def set_environment! + Rails.env = environment + end + + def start + set_environment! if environment? + + if sandbox? + puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})" + puts "Any modifications you make will be rolled back on exit" + else + puts "Loading #{Rails.env} environment (Rails #{Rails.version})" + end + + if defined?(console::ExtendCommandBundle) + console::ExtendCommandBundle.include(Rails::ConsoleMethods) + end + console.start + end + end + + module Command + class ConsoleCommand < Base # :nodoc: + include EnvironmentArgument + + class_option :sandbox, aliases: "-s", type: :boolean, default: false, + desc: "Rollback database modifications on exit." + + def initialize(args = [], local_options = {}, config = {}) + console_options = [] + + # For the same behavior as OptionParser, leave only options after "--" in ARGV. + termination = local_options.find_index("--") + if termination + console_options = local_options[termination + 1..-1] + local_options = local_options[0...termination] + end + + ARGV.replace(console_options) + super(args, local_options, config) + end + + def perform + extract_environment_option_from_argument + + # RAILS_ENV needs to be set before config/application is required. + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + Rails::Console.start(Rails.application, options) + end + end + end +end diff --git a/railties/lib/rails/commands/console_helper.rb b/railties/lib/rails/commands/console_helper.rb deleted file mode 100644 index 8ee0b60012..0000000000 --- a/railties/lib/rails/commands/console_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'active_support/concern' - -module Rails - module ConsoleHelper # :nodoc: - extend ActiveSupport::Concern - - module ClassMethods - def start(*args) - new(*args).start - end - - private - def set_options_env(arguments, options) - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env - else - options[:environment] = %w(production development test).detect { |e| e =~ /^#{env}/ } || env - end - end - options - end - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end - end - - def environment - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - end - end -end
\ No newline at end of file diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE new file mode 100644 index 0000000000..c8d3fb9eda --- /dev/null +++ b/railties/lib/rails/commands/credentials/USAGE @@ -0,0 +1,58 @@ +=== Storing Encrypted Credentials in Source Control + +The Rails `credentials` commands provide access to encrypted credentials, +so you can safely store access tokens, database passwords, and the like +safely inside the app without relying on a mess of ENVs. + +This also allows for atomic deploys: no need to coordinate key changes +to get everything working as the keys are shipped with the code. + +=== Setup + +Applications after Rails 5.2 automatically have a basic credentials file generated +that just contains the secret_key_base used by MessageVerifiers/MessageEncryptors, like the ones +signing and encrypting cookies. + +For applications created prior to Rails 5.2, we'll automatically generate a new +credentials file in `config/credentials.yml.enc` the first time you run `rails credentials:edit`. +If you didn't have a master key saved in `config/master.key`, that'll be created too. + +Don't lose this master key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +credentials. + +Don't commit the key! Add `config/master.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the master key in `ENV["RAILS_MASTER_KEY"]`, if that's easier to manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="very-secret-and-secure" server.start + +=== Editing Credentials + +This will open a temporary file in `$EDITOR` with the decrypted contents to edit +the encrypted credentials. + +When the temporary file is next saved the contents are encrypted and written to +`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials +from leaking. + +=== Environment Specific Credentials + +The `credentials` command supports passing an `--environment` option to create an +environment specific override. That override will take precedence over the +global `config/credentials.yml.enc` file when running in that environment. So: + + rails credentials:edit --environment development + +will create `config/credentials/development.yml.enc` with the corresponding +encryption key in `config/credentials/development.key` if the credentials file +doesn't exist. + +The encryption key can also be put in `ENV["RAILS_MASTER_KEY"]`, which takes +precedence over the file encryption key. + +In addition to that, the default credentials lookup paths can be overridden through +`config.credentials.content_path` and `config.credentials.key_path`. diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb new file mode 100644 index 0000000000..e23a1b3008 --- /dev/null +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "active_support" +require "rails/command/helpers/editor" +require "rails/command/environment_argument" + +module Rails + module Command + class CredentialsCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + include EnvironmentArgument + + self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def edit + extract_environment_option_from_argument(default_environment: nil) + require_application! + + ensure_editor_available(command: "bin/rails credentials:edit") || (return) + + ensure_encryption_key_has_been_added if credentials.key.nil? + ensure_credentials_have_been_added + + catch_editing_exceptions do + change_credentials_in_system_editor + end + + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?" + end + + def show + extract_environment_option_from_argument(default_environment: nil) + require_application! + + say credentials.read.presence || missing_credentials_message + end + + private + def credentials + Rails.application.encrypted(content_path, key_path: key_path) + end + + def ensure_encryption_key_has_been_added + encryption_key_file_generator.add_key_file(key_path) + encryption_key_file_generator.ignore_key_file(key_path) + end + + def ensure_credentials_have_been_added + if options[:environment] + encrypted_file_generator.add_encrypted_file_silently(content_path, key_path) + else + credentials_generator.add_credentials_file_silently + end + end + + def change_credentials_in_system_editor + credentials.change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + def missing_credentials_message + if credentials.key.nil? + "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" + else + "File '#{content_path}' does not exist. Use `rails credentials:edit` to change that." + end + end + + + def content_path + options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" + end + + def key_path + options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" + end + + + def encryption_key_file_generator + require "rails/generators" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" + + Rails::Generators::EncryptionKeyFileGenerator.new + end + + def encrypted_file_generator + require "rails/generators" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" + + Rails::Generators::EncryptedFileGenerator.new + end + + def credentials_generator + require "rails/generators" + require "rails/generators/rails/credentials/credentials_generator" + + Rails::Generators::CredentialsGenerator.new + end + end + end +end diff --git a/railties/lib/rails/commands/db/system/change/change_command.rb b/railties/lib/rails/commands/db/system/change/change_command.rb new file mode 100644 index 0000000000..760c229c07 --- /dev/null +++ b/railties/lib/rails/commands/db/system/change/change_command.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/rails/db/system/change/change_generator" + +module Rails + module Command + module Db + module System + class ChangeCommand < Base # :nodoc: + class_option :to, desc: "The database system to switch to." + + def perform + Rails::Generators::Db::System::ChangeGenerator.start + end + end + end + end + end +end diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb deleted file mode 100644 index 2c36edfa3f..0000000000 --- a/railties/lib/rails/commands/dbconsole.rb +++ /dev/null @@ -1,173 +0,0 @@ -require 'erb' -require 'yaml' -require 'optparse' -require 'rails/commands/console_helper' - -module Rails - class DBConsole - include ConsoleHelper - - attr_reader :arguments - - class << self - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails dbconsole [environment] [options]" - opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| - options['include_password'] = true - end - - opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], - "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| - options['mode'] = mode - end - - opt.on("--header") do |h| - options['header'] = h - end - - opt.on("-h", "--help", "Show this help message.") do - puts opt - exit - end - - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development" - ) { |v| options[:environment] = v.strip } - - opt.parse!(arguments) - abort opt.to_s unless (0..1).include?(arguments.size) - end - - set_options_env(arguments, options) - end - end - - def initialize(arguments = ARGV) - @arguments = arguments - end - - def start - options = self.class.parse_arguments(arguments) - ENV['RAILS_ENV'] = options[:environment] || environment - - case config["adapter"] - when /^(jdbc)?mysql/ - args = { - 'host' => '--host', - 'port' => '--port', - 'socket' => '--socket', - 'username' => '--user', - 'encoding' => '--default-character-set', - 'sslca' => '--ssl-ca', - 'sslcert' => '--ssl-cert', - 'sslcapath' => '--ssl-capath', - 'sslcipher' => '--ssl-cipher', - 'sslkey' => '--ssl-key' - }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact - - if config['password'] && options['include_password'] - args << "--password=#{config['password']}" - elsif config['password'] && !config['password'].to_s.empty? - args << "-p" - end - - args << config['database'] - - find_cmd_and_exec(['mysql', 'mysql5'], *args) - - when /^postgres|^postgis/ - ENV['PGUSER'] = config["username"] if config["username"] - ENV['PGHOST'] = config["host"] if config["host"] - ENV['PGPORT'] = config["port"].to_s if config["port"] - ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && options['include_password'] - find_cmd_and_exec('psql', config["database"]) - - when "sqlite3" - args = [] - - args << "-#{options['mode']}" if options['mode'] - args << "-header" if options['header'] - args << File.expand_path(config['database'], Rails.respond_to?(:root) ? Rails.root : nil) - - find_cmd_and_exec('sqlite3', *args) - - when "oracle", "oracle_enhanced" - logon = "" - - if config['username'] - logon = config['username'] - logon << "/#{config['password']}" if config['password'] && options['include_password'] - logon << "@#{config['database']}" if config['database'] - end - - find_cmd_and_exec('sqlplus', logon) - - when "sqlserver" - args = [] - - args += ["-D", "#{config['database']}"] if config['database'] - args += ["-U", "#{config['username']}"] if config['username'] - args += ["-P", "#{config['password']}"] if config['password'] - - if config['host'] - host_arg = "#{config['host']}" - host_arg << ":#{config['port']}" if config['port'] - args += ["-S", host_arg] - end - - find_cmd_and_exec("sqsh", *args) - - else - abort "Unknown command-line client for #{config['database']}." - end - end - - def config - @config ||= begin - if configurations[environment].blank? - raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" - else - configurations[environment] - end - end - end - - def environment - Rails.respond_to?(:env) ? Rails.env : super - end - - protected - def configurations - require APP_PATH - ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.configurations - end - - def find_cmd_and_exec(commands, *args) - commands = Array(commands) - - dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - unless (ext = RbConfig::CONFIG['EXEEXT']).empty? - commands = commands.map{|cmd| "#{cmd}#{ext}"} - end - - full_path_command = nil - found = commands.detect do |cmd| - dirs_on_path.detect do |path| - full_path_command = File.join(path, cmd) - File.file?(full_path_command) && File.executable?(full_path_command) - end - end - - if found - exec full_path_command, *args - else - abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end - end - end -end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb new file mode 100644 index 0000000000..72f3235ce3 --- /dev/null +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require "active_support/deprecation" +require "active_support/core_ext/string/filters" +require "rails/command/environment_argument" + +module Rails + class DBConsole + def self.start(*args) + new(*args).start + end + + def initialize(options = {}) + @options = options + end + + def start + ENV["RAILS_ENV"] ||= @options[:environment] || environment + + case config["adapter"] + when /^(jdbc)?mysql/ + args = { + "host" => "--host", + "port" => "--port", + "socket" => "--socket", + "username" => "--user", + "encoding" => "--default-character-set", + "sslca" => "--ssl-ca", + "sslcert" => "--ssl-cert", + "sslcapath" => "--ssl-capath", + "sslcipher" => "--ssl-cipher", + "sslkey" => "--ssl-key" + }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact + + if config["password"] && @options["include_password"] + args << "--password=#{config['password']}" + elsif config["password"] && !config["password"].to_s.empty? + args << "-p" + end + + args << config["database"] + + find_cmd_and_exec(["mysql", "mysql5"], *args) + + when /^postgres|^postgis/ + ENV["PGUSER"] = config["username"] if config["username"] + ENV["PGHOST"] = config["host"] if config["host"] + ENV["PGPORT"] = config["port"].to_s if config["port"] + ENV["PGPASSWORD"] = config["password"].to_s if config["password"] && @options["include_password"] + find_cmd_and_exec("psql", config["database"]) + + when "sqlite3" + args = [] + + args << "-#{@options['mode']}" if @options["mode"] + args << "-header" if @options["header"] + args << File.expand_path(config["database"], Rails.respond_to?(:root) ? Rails.root : nil) + + find_cmd_and_exec("sqlite3", *args) + + when "oracle", "oracle_enhanced" + logon = "" + + if config["username"] + logon = config["username"].dup + logon << "/#{config['password']}" if config["password"] && @options["include_password"] + logon << "@#{config['database']}" if config["database"] + end + + find_cmd_and_exec("sqlplus", logon) + + when "sqlserver" + args = [] + + args += ["-D", "#{config['database']}"] if config["database"] + args += ["-U", "#{config['username']}"] if config["username"] + args += ["-P", "#{config['password']}"] if config["password"] + + if config["host"] + host_arg = +"#{config['host']}" + host_arg << ":#{config['port']}" if config["port"] + args += ["-S", host_arg] + end + + find_cmd_and_exec("sqsh", *args) + + else + abort "Unknown command-line client for #{config['database']}." + end + end + + def config + @config ||= begin + # We need to check whether the user passed the database the + # first time around to show a consistent error message to people + # relying on 2-level database configuration. + if @options["database"] && configurations[database].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{database}' database is not configured. Available configuration: #{configurations.inspect}" + elsif configurations[environment].blank? && configurations[database].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" + else + configurations[database] || configurations[environment].presence + end + end + end + + def environment + Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment + end + + def database + @options.fetch(:database, "primary") + end + + private + def configurations # :doc: + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end + + def find_cmd_and_exec(commands, *args) # :doc: + commands = Array(commands) + + dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR) + unless (ext = RbConfig::CONFIG["EXEEXT"]).empty? + commands = commands.map { |cmd| "#{cmd}#{ext}" } + end + + full_path_command = nil + found = commands.detect do |cmd| + dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.file?(full_path_command) && File.executable?(full_path_command) + end + end + + if found + exec full_path_command, *args + else + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") + end + end + end + + module Command + class DbconsoleCommand < Base # :nodoc: + include EnvironmentArgument + + class_option :include_password, aliases: "-p", type: :boolean, + desc: "Automatically provide the password from database.yml" + + class_option :mode, enum: %w( html list line column ), type: :string, + desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." + + class_option :header, type: :boolean + + class_option :connection, aliases: "-c", type: :string, + desc: "Specifies the connection to use." + + class_option :database, aliases: "--db", type: :string, + desc: "Specifies the database to use." + + def perform + extract_environment_option_from_argument + + # RAILS_ENV needs to be set before config/application is required. + ENV["RAILS_ENV"] = options[:environment] + + if options["connection"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `connection` option is deprecated and will be removed in Rails 6.1. Please use `database` option instead. + MSG + options["database"] = options["connection"] + end + + require_application_and_environment! + Rails::DBConsole.start(options) + end + end + end +end diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb deleted file mode 100644 index ce26cc3fde..0000000000 --- a/railties/lib/rails/commands/destroy.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'rails/generators' - -#if no argument/-h/--help is passed to rails destroy command, then -#it generates the help associated. -if [nil, "-h", "--help"].include?(ARGV.first) - Rails::Generators.help 'destroy' - exit -end - -name = ARGV.shift -Rails::Generators.invoke name, ARGV, behavior: :revoke, destination_root: Rails.root diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb new file mode 100644 index 0000000000..dd432d28fd --- /dev/null +++ b/railties/lib/rails/commands/destroy/destroy_command.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails/generators" + +module Rails + module Command + class DestroyCommand < Base # :nodoc: + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end + end + + def perform(*) + generator = args.shift + return help unless generator + + require_application_and_environment! + load_generators + + Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails::Command.root + end + end + end +end diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb new file mode 100644 index 0000000000..a3f02f3172 --- /dev/null +++ b/railties/lib/rails/commands/dev/dev_command.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "rails/dev_caching" + +module Rails + module Command + class DevCommand < Base # :nodoc: + def help + say "rails dev:cache # Toggle development mode caching on/off." + end + + def cache + Rails::DevCaching.enable_by_file + end + end + end +end diff --git a/railties/lib/rails/commands/encrypted/USAGE b/railties/lib/rails/commands/encrypted/USAGE new file mode 100644 index 0000000000..253eec2378 --- /dev/null +++ b/railties/lib/rails/commands/encrypted/USAGE @@ -0,0 +1,28 @@ +=== Storing Encrypted Files in Source Control + +The Rails `encrypted` commands provide access to encrypted files or configurations. +See the `Rails.application.encrypted` documentation for using them in your app. + +=== Encryption Keys + +By default, Rails looks for the encryption key in `config/master.key` or +`ENV["RAILS_MASTER_KEY"]`, but that lookup can be overridden with `--key`: + + rails encrypted:edit config/encrypted_file.yml.enc --key config/encrypted_file.key + +Don't commit the key! Add it to your source control's ignore file. If you use +Git, Rails handles this for you. + +=== Editing Files + +To edit or create an encrypted file use: + + rails encrypted:edit config/encrypted_file.yml.enc + +This opens a temporary file in `$EDITOR` with the decrypted contents for editing. + +=== Viewing Files + +To print the decrypted contents of an encrypted file use: + + rails encrypted:show config/encrypted_file.yml.enc diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb new file mode 100644 index 0000000000..f10a07cdf8 --- /dev/null +++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "pathname" +require "active_support" +require "rails/command/helpers/editor" + +module Rails + module Command + class EncryptedCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + + class_option :key, aliases: "-k", type: :string, + default: "config/master.key", desc: "The Rails.root relative path to the encryption key" + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def edit(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + ensure_editor_available(command: "bin/rails encrypted:edit") || (return) + ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil? + ensure_encrypted_file_has_been_added(file_path, options[:key]) + + catch_editing_exceptions do + change_encrypted_file_in_system_editor(file_path, options[:key]) + end + + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?" + end + + def show(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path) + end + + private + def ensure_encryption_key_has_been_added(key_path) + encryption_key_file_generator.add_key_file(key_path) + encryption_key_file_generator.ignore_key_file(key_path) + end + + def ensure_encrypted_file_has_been_added(file_path, key_path) + encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) + end + + def change_encrypted_file_in_system_editor(file_path, key_path) + Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + + def encryption_key_file_generator + require "rails/generators" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" + + Rails::Generators::EncryptionKeyFileGenerator.new + end + + def encrypted_file_generator + require "rails/generators" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" + + Rails::Generators::EncryptedFileGenerator.new + end + + def missing_encrypted_message(key:, key_path:, file_path:) + if key.nil? + "Missing '#{key_path}' to decrypt data. See `rails encrypted:help`" + else + "File '#{file_path}' does not exist. Use `rails encrypted:edit #{file_path}` to change that." + end + end + end + end +end diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb deleted file mode 100644 index 926c36b967..0000000000 --- a/railties/lib/rails/commands/generate.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rails/generators' - -#if no argument/-h/--help is passed to rails generate command, then -#it generates the help associated. -if [nil, "-h", "--help"].include?(ARGV.first) - Rails::Generators.help 'generate' - exit -end - -name = ARGV.shift - -root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root -Rails::Generators.invoke name, ARGV, behavior: :invoke, destination_root: root diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb new file mode 100644 index 0000000000..93d7a0ce3a --- /dev/null +++ b/railties/lib/rails/commands/generate/generate_command.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rails/generators" + +module Rails + module Command + class GenerateCommand < Base # :nodoc: + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end + end + + def perform(*) + generator = args.shift + return help unless generator + + require_application_and_environment! + load_generators + + ARGV.shift + + Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root + end + end + end +end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE new file mode 100644 index 0000000000..8eb98319d2 --- /dev/null +++ b/railties/lib/rails/commands/help/USAGE @@ -0,0 +1,16 @@ +The most common rails commands are: + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + test Run tests except system tests (short-cut alias: "t") + test:system Run system tests + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") +<% unless engine? %> + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" +<% end %> + +All commands can be run with -h (or --help) for more information. +In addition to those commands, there are: + diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb new file mode 100644 index 0000000000..9df34e9b79 --- /dev/null +++ b/railties/lib/rails/commands/help/help_command.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Rails + module Command + class HelpCommand < Base # :nodoc: + hide_command! + + def help(*) + say self.class.desc + + Rails::Command.print_commands + end + end + end +end diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb new file mode 100644 index 0000000000..bd2f3bed67 --- /dev/null +++ b/railties/lib/rails/commands/initializers/initializers_command.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "rails/command/environment_argument" + +module Rails + module Command + class InitializersCommand < Base # :nodoc: + include EnvironmentArgument + + desc "initializers", "Print out all defined initializers in the order they are invoked by Rails." + def perform + extract_environment_option_from_argument + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + + Rails.application.initializers.tsort_each do |initializer| + say "#{initializer.context_class}.#{initializer.name}" + end + end + end + end +end diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb new file mode 100644 index 0000000000..a4f2081510 --- /dev/null +++ b/railties/lib/rails/commands/new/new_command.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Rails + module Command + class NewCommand < Base # :nodoc: + no_commands do + def help + Rails::Command.invoke :application, [ "--help" ] + end + end + + def perform(*) + say "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" + say "Type 'rails' for help." + exit 1 + end + end + end +end diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb new file mode 100644 index 0000000000..64b339b3cd --- /dev/null +++ b/railties/lib/rails/commands/notes/notes_command.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails/source_annotation_extractor" + +module Rails + module Command + class NotesCommand < Base # :nodoc: + class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO) + + def perform(*) + require_application_and_environment! + + deprecation_warning + display_annotations + end + + private + def display_annotations + annotations = options[:annotations] + tag = (annotations.length > 1) + + Rails::SourceAnnotationExtractor.enumerate annotations.join("|"), tag: tag, dirs: directories + end + + def directories + Rails::SourceAnnotationExtractor::Annotation.directories + source_annotation_directories + end + + def deprecation_warning + return if source_annotation_directories.empty? + ActiveSupport::Deprecation.warn("`SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1. You can add default directories by using config.annotations.register_directories instead.") + end + + def source_annotation_directories + ENV["SOURCE_ANNOTATION_DIRECTORIES"].to_s.split(",") + end + end + end +end diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb deleted file mode 100644 index 52d8966ead..0000000000 --- a/railties/lib/rails/commands/plugin.rb +++ /dev/null @@ -1,23 +0,0 @@ -if ARGV.first != "new" - ARGV[0] = "--help" -else - ARGV.shift - unless ARGV.delete("--no-rc") - customrc = ARGV.index{ |x| x.include?("--rc=") } - railsrc = if customrc - File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, "")) - else - File.join(File.expand_path("~"), '.railsrc') - end - if File.exist?(railsrc) - extra_args_string = File.read(railsrc) - extra_args = extra_args_string.split(/\n+/).flat_map(&:split) - puts "Using #{extra_args.join(" ")} from #{railsrc}" - ARGV.insert(1, *extra_args) - end - end -end - -require 'rails/generators' -require 'rails/generators/rails/plugin/plugin_generator' -Rails::Generators::PluginGenerator.start diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb new file mode 100644 index 0000000000..96187aa952 --- /dev/null +++ b/railties/lib/rails/commands/plugin/plugin_command.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Rails + module Command + class PluginCommand < Base # :nodoc: + hide_command! + + def help + run_plugin_generator %w( --help ) + end + + def self.banner(*) # :nodoc: + "#{executable} new [options]" + end + + class_option :rc, type: :string, default: File.join("~", ".railsrc"), + desc: "Initialize the plugin command with previous defaults. Uses .railsrc in your home directory by default." + + class_option :no_rc, desc: "Skip evaluating .railsrc." + + def perform(type = nil, *plugin_args) + plugin_args << "--help" unless type == "new" + + unless options.key?("no_rc") # Thor's not so indifferent access hash. + railsrc = File.expand_path(options[:rc]) + + if File.exist?(railsrc) + extra_args = File.read(railsrc).split(/\n+/).flat_map(&:split) + say "Using #{extra_args.join(" ")} from #{railsrc}" + plugin_args.insert(1, *extra_args) + end + end + + run_plugin_generator plugin_args + end + + private + def run_plugin_generator(plugin_args) + require "rails/generators" + require "rails/generators/rails/plugin/plugin_generator" + Rails::Generators::PluginGenerator.start plugin_args + end + end + end +end diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb new file mode 100644 index 0000000000..535df0c430 --- /dev/null +++ b/railties/lib/rails/commands/rake/rake_command.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Rails + module Command + class RakeCommand < Base # :nodoc: + extend Rails::Command::Actions + + namespace "rake" + + class << self + def printing_commands + formatted_rake_tasks.map(&:first) + end + + def perform(task, *) + require_rake + + ARGV.unshift(task) # Prepend the task, so Rake knows how to run it. + + Rake.application.standard_exception_handling do + Rake.application.init("rails") + Rake.application.load_rakefile + Rake.application.top_level + end + end + + private + def rake_tasks + require_rake + + return @rake_tasks if defined?(@rake_tasks) + + require_application_and_environment! + + Rake::TaskManager.record_task_metadata = true + Rake.application.instance_variable_set(:@name, "rails") + load_tasks + @rake_tasks = Rake.application.tasks.select(&:comment) + end + + def formatted_rake_tasks + rake_tasks.map { |t| [ t.name_with_args, t.comment ] } + end + + def require_rake + require "rake" # Defer booting Rake until we know it's needed. + end + end + end + end +end diff --git a/railties/lib/rails/commands/rake_proxy.rb b/railties/lib/rails/commands/rake_proxy.rb deleted file mode 100644 index f7d5df6b2f..0000000000 --- a/railties/lib/rails/commands/rake_proxy.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'rake' -require 'active_support' - -module Rails - module RakeProxy #:nodoc: - private - def run_rake_task(command) - ARGV.unshift(command) # Prepend the command, so Rake knows how to run it. - - Rake.application.standard_exception_handling do - Rake.application.init('rails') - Rake.application.load_rakefile - Rake.application.top_level - end - end - - def rake_tasks - return @rake_tasks if defined?(@rake_tasks) - - ActiveSupport::Deprecation.silence do - require_application_and_environment! - end - - Rake::TaskManager.record_task_metadata = true - Rake.application.instance_variable_set(:@name, 'rails') - Rails.application.load_tasks - @rake_tasks = Rake.application.tasks.select(&:comment) - end - - def formatted_rake_tasks - rake_tasks.map { |t| [ t.name_with_args, t.comment ] } - end - end -end diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb new file mode 100644 index 0000000000..b592a5212f --- /dev/null +++ b/railties/lib/rails/commands/routes/routes_command.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/command" + +module Rails + module Command + class RoutesCommand < Base # :nodoc: + class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController." + class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern." + class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained." + + def perform(*) + require_application_and_environment! + require "action_dispatch/routing/inspector" + + say inspector.format(formatter, routes_filter) + end + + private + def inspector + ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes) + end + + def formatter + if options.key?("expanded") + ActionDispatch::Routing::ConsoleFormatter::Expanded.new + else + ActionDispatch::Routing::ConsoleFormatter::Sheet.new + end + end + + def routes_filter + options.symbolize_keys.slice(:controller, :grep) + end + end + end +end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb deleted file mode 100644 index f9c183ac86..0000000000 --- a/railties/lib/rails/commands/runner.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'optparse' - -options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup } -code_or_file = nil -command = 'bin/rails runner' - -if ARGV.first.nil? - ARGV.push "-h" -end - -ARGV.clone.options do |opts| - opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]" - - opts.separator "" - - opts.on("-e", "--environment=name", String, - "Specifies the environment for the runner to operate under (test/development/production).", - "Default: development") { |v| options[:environment] = v } - - opts.separator "" - - opts.on("-h", "--help", - "Show this help message.") { $stdout.puts opts; exit } - - opts.separator "" - opts.separator "Examples: " - - opts.separator " rails runner 'puts Rails.env'" - opts.separator " This runs the code `puts Rails.env` after loading the app" - opts.separator "" - opts.separator " rails runner path/to/filename.rb" - opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app" - - if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ - opts.separator "" - opts.separator "You can also use runner as a shebang line for your executables:" - opts.separator " -------------------------------------------------------------" - opts.separator " #!/usr/bin/env #{File.expand_path(command)}" - opts.separator "" - opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }" - opts.separator " -------------------------------------------------------------" - end - - opts.order! { |o| code_or_file ||= o } rescue retry -end - -ARGV.delete(code_or_file) - -ENV["RAILS_ENV"] = options[:environment] - -require APP_PATH -Rails.application.require_environment! -Rails.application.load_runner - -if code_or_file.nil? - $stderr.puts "Run '#{command} -h' for help." - exit 1 -elsif File.exist?(code_or_file) - $0 = code_or_file - Kernel.load code_or_file -else - begin - eval(code_or_file, binding, __FILE__, __LINE__) - rescue SyntaxError, NameError - $stderr.puts "Please specify a valid ruby command or the path of a script to run." - $stderr.puts "Run '#{command} -h' for help." - exit 1 - end -end diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE new file mode 100644 index 0000000000..24b60037f0 --- /dev/null +++ b/railties/lib/rails/commands/runner/USAGE @@ -0,0 +1,20 @@ +Examples: + +Run `puts Rails.env` after loading the app: + + <%= executable %> 'puts Rails.env' + +Run the Ruby file located at `path/to/filename.rb` after loading the app: + + <%= executable %> path/to/filename.rb + +Run the Ruby script read from stdin after loading the app: + <%= executable %> - + +<% unless Gem.win_platform? %> +You can also use the runner command as a shebang line for your executables: + + #!/usr/bin/env <%= File.expand_path(executable) %> + + Product.all.each { |p| p.price *= 2 ; p.save! } +<% end %> diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb new file mode 100644 index 0000000000..40fb5e4d89 --- /dev/null +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "rails/command/environment_argument" + +module Rails + module Command + class RunnerCommand < Base # :nodoc: + include EnvironmentArgument + + self.environment_desc = "The environment for the runner to operate under (test/development/production)" + + no_commands do + def help + super + say self.class.desc + end + end + + def self.banner(*) + "#{super} [<'Some.ruby(code)'> | <filename.rb> | -]" + end + + def perform(code_or_file = nil, *command_argv) + extract_environment_option_from_argument + + unless code_or_file + help + exit 1 + end + + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + Rails.application.load_runner + + ARGV.replace(command_argv) + + if code_or_file == "-" + eval($stdin.read, TOPLEVEL_BINDING, "stdin") + elsif File.exist?(code_or_file) + $0 = code_or_file + Kernel.load code_or_file + else + begin + eval(code_or_file, TOPLEVEL_BINDING, __FILE__, __LINE__) + rescue SyntaxError, NameError => e + error "Please specify a valid ruby command or the path of a script to run." + error "Run '#{self.class.executable} -h' for help." + error "" + error e + exit 1 + end + end + end + end + end +end diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE new file mode 100644 index 0000000000..e205cdc001 --- /dev/null +++ b/railties/lib/rails/commands/secrets/USAGE @@ -0,0 +1,60 @@ +=== Storing Encrypted Secrets in Source Control + +The Rails `secrets` commands helps encrypting secrets to slim a production +environment's `ENV` hash. It's also useful for atomic deploys: no need to +coordinate key changes to get everything working as the keys are shipped +with the code. + +=== Setup + +Run `rails secrets:setup` to opt in and generate the `config/secrets.yml.key` +and `config/secrets.yml.enc` files. + +The latter contains all the keys to be encrypted while the former holds the +encryption key. + +Don't lose the key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +secrets. +Don't commit the key! Add `config/secrets.yml.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to +manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start + + +The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`: + + production: + secret_key_base: so-secret-very-hidden-wow + payment_processing_gateway_key: much-safe-very-gaedwey-wow + +But that's where the similarities between `secrets.yml` and `secrets.yml.enc` +end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and +be encrypted. + +A `shared:` top level key is also supported such that any keys there is merged +into the other environments. + +Additionally, Rails won't read encrypted secrets out of the box even if you have +the key. Add this: + + config.read_encrypted_secrets = true + +to the environment you'd like to read encrypted secrets. `rails secrets:setup` +inserts this into the production environment by default. + +=== Editing Secrets + +After `rails secrets:setup`, run `rails secrets:edit`. + +That command opens a temporary file in `$EDITOR` with the decrypted contents of +`config/secrets.yml.enc` to edit the encrypted secrets. + +When the temporary file is next saved the contents are encrypted and written to +`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets +from leaking. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb new file mode 100644 index 0000000000..2eebc0f35f --- /dev/null +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "active_support" +require "rails/secrets" + +module Rails + module Command + class SecretsCommand < Rails::Command::Base # :nodoc: + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def setup + deprecate_in_favor_of_credentials_and_exit + end + + def edit + if ENV["EDITOR"].to_s.empty? + say "No $EDITOR to open decrypted secrets in. Assign one like this:" + say "" + say %(EDITOR="mate --wait" rails secrets:edit) + say "" + say "For editors that fork and exit immediately, it's important to pass a wait flag," + say "otherwise the secrets will be saved immediately with no chance to edit." + + return + end + + require_application_and_environment! + + Rails::Secrets.read_for_editing do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + + say "New secrets encrypted and saved." + rescue Interrupt + say "Aborted changing encrypted secrets: nothing saved." + rescue Rails::Secrets::MissingKeyError => error + say error.message + rescue Errno::ENOENT => error + if /secrets\.yml\.enc/.match?(error.message) + deprecate_in_favor_of_credentials_and_exit + else + raise + end + end + + def show + say Rails::Secrets.read + end + + private + def deprecate_in_favor_of_credentials_and_exit + say "Encrypted secrets is deprecated in favor of credentials. Run:" + say "rails credentials:help" + + exit 1 + end + end + end +end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb deleted file mode 100644 index 7418dff18b..0000000000 --- a/railties/lib/rails/commands/server.rb +++ /dev/null @@ -1,139 +0,0 @@ -require 'fileutils' -require 'optparse' -require 'action_dispatch' -require 'rails' -require 'rails/dev_caching' - -module Rails - class Server < ::Rack::Server - class Options - DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze - - def parse!(args) - args, options = args.dup, {} - - option_parser(options).parse! args - - options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" - options[:server] = args.shift - options - end - - private - - def option_parser(options) - OptionParser.new do |opts| - opts.banner = "Usage: rails server [mongrel, thin etc] [options]" - opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } - opts.on("-b", "--binding=IP", String, - "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v } - opts.on("-c", "--config=file", String, - "Uses a custom rackup configuration.") { |v| options[:config] = v } - opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true } - opts.on("-e", "--environment=name", String, - "Specifies the environment to run this server under (test/development/production).", - "Default: development") { |v| options[:environment] = v } - opts.on("-P", "--pid=pid", String, - "Specifies the PID file.", - "Default: tmp/pids/server.pid") { |v| options[:pid] = v } - opts.on("-C", "--[no-]dev-caching", - "Specifies whether to perform caching in development.", - "true or false") { |v| options[:caching] = v } - - opts.separator "" - - opts.on("-h", "--help", "Shows this help message.") { puts opts; exit } - end - end - end - - def initialize(*) - super - set_environment - end - - # TODO: this is no longer required but we keep it for the moment to support older config.ru files. - def app - @app ||= begin - app = super - app.respond_to?(:to_app) ? app.to_app : app - end - end - - def opt_parser - Options.new - end - - def set_environment - ENV["RAILS_ENV"] ||= options[:environment] - end - - def start - print_boot_information - trap(:INT) { exit } - create_tmp_directories - setup_dev_caching - log_to_stdout if options[:log_stdout] - - super - ensure - # The '-h' option calls exit before @options is set. - # If we call 'options' with it unset, we get double help banners. - puts 'Exiting' unless @options && options[:daemonize] - end - - def middleware - Hash.new([]) - end - - def default_options - super.merge({ - Port: ENV.fetch('PORT', 3000).to_i, - DoNotReverseLookup: true, - environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, - daemonize: false, - caching: nil, - pid: Options::DEFAULT_PID_PATH, - restart_cmd: restart_command - }) - end - - private - - def setup_dev_caching - if options[:environment] == "development" - Rails::DevCaching.enable_by_argument(options[:caching]) - end - end - - def print_boot_information - url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" - puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" - puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" - puts "=> Run `rails server -h` for more startup options" - end - - def create_tmp_directories - %w(cache pids sockets).each do |dir_to_make| - FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) - end - end - - def log_to_stdout - wrapped_app # touch the app so the logger is set up - - console = ActiveSupport::Logger.new(STDOUT) - console.formatter = Rails.logger.formatter - console.level = Rails.logger.level - - unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) - Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) - end - end - - def restart_command - "bin/rails server #{ARGV.join(' ')}" - end - end -end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb new file mode 100644 index 0000000000..982b83ead5 --- /dev/null +++ b/railties/lib/rails/commands/server/server_command.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +require "fileutils" +require "action_dispatch" +require "rails" +require "active_support/deprecation" +require "active_support/core_ext/string/filters" +require "rails/dev_caching" +require "rails/command/environment_argument" + +module Rails + class Server < ::Rack::Server + class Options + def parse!(args) + Rails::Command::ServerCommand.new([], args).server_options + end + end + + def initialize(options = nil) + @default_options = options || {} + super(@default_options) + set_environment + end + + def opt_parser + Options.new + end + + def set_environment + ENV["RAILS_ENV"] ||= options[:environment] + end + + def start(after_stop_callback = nil) + trap(:INT) { exit } + create_tmp_directories + setup_dev_caching + log_to_stdout if options[:log_stdout] + + super() + ensure + after_stop_callback.call if after_stop_callback + end + + def serveable? # :nodoc: + server + true + rescue LoadError, NameError + false + end + + def middleware + Hash.new([]) + end + + def default_options + super.merge(@default_options) + end + + def served_url + "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma? + end + + private + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + + def create_tmp_directories + %w(cache pids sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make)) + end + end + + def log_to_stdout + wrapped_app # touch the app so the logger is set up + + console = ActiveSupport::Logger.new(STDOUT) + console.formatter = Rails.logger.formatter + console.level = Rails.logger.level + + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end + end + + def use_puma? + server.to_s == "Rack::Handler::Puma" + end + end + + module Command + class ServerCommand < Base # :nodoc: + include EnvironmentArgument + + # Hard-coding a bunch of handlers here as we don't have a public way of + # querying them from the Rack::Handler registry. + RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn) + + DEFAULT_PORT = 3000 + DEFAULT_PID_PATH = "tmp/pids/server.pid" + + argument :using, optional: true + + class_option :port, aliases: "-p", type: :numeric, + desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port + class_option :binding, aliases: "-b", type: :string, + desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", + banner: :IP + class_option :config, aliases: "-c", type: :string, default: "config.ru", + desc: "Uses a custom rackup configuration.", banner: :file + class_option :daemon, aliases: "-d", type: :boolean, default: false, + desc: "Runs server as a Daemon." + class_option :using, aliases: "-u", type: :string, + desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name + class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH, + desc: "Specifies the PID file." + class_option :dev_caching, aliases: "-C", type: :boolean, default: nil, + desc: "Specifies whether to perform caching in development." + class_option :restart, type: :boolean, default: nil, hide: true + class_option :early_hints, type: :boolean, default: nil, desc: "Enables HTTP/2 early hints." + class_option :log_to_stdout, type: :boolean, default: nil, optional: true, + desc: "Whether to log to stdout. Enabled by default in development when not daemonized." + + def initialize(args, local_options, *) + super + + @original_options = local_options - %w( --restart ) + deprecate_positional_rack_server_and_rewrite_to_option(@original_options) + end + + def perform + extract_environment_option_from_argument + set_application_directory! + prepare_restart + + Rails::Server.new(server_options).tap do |server| + # Require application after server sets environment to propagate + # the --environment option. + require APP_PATH + Dir.chdir(Rails.application.root) + + if server.serveable? + print_boot_information(server.server, server.served_url) + after_stop_callback = -> { say "Exiting" unless options[:daemon] } + server.start(after_stop_callback) + else + say rack_server_suggestion(using) + end + end + end + + no_commands do + def server_options + { + user_supplied_options: user_supplied_options, + server: using, + log_stdout: log_to_stdout?, + Port: port, + Host: host, + DoNotReverseLookup: true, + config: options[:config], + environment: environment, + daemonize: options[:daemon], + pid: pid, + caching: options[:dev_caching], + restart_cmd: restart_command, + early_hints: early_hints + } + end + end + + private + def user_supplied_options + @user_supplied_options ||= begin + # Convert incoming options array to a hash of flags + # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} + user_flag = {} + @original_options.each do |command| + if command.to_s.start_with?("--") + option = command.split("=")[0] + user_flag[option] = true + elsif command =~ /\A(-.)/ + user_flag[Regexp.last_match[0]] = true + end + end + + # Collect all options that the user has explicitly defined so we can + # differentiate them from defaults + user_supplied_options = [] + self.class.class_options.select do |key, option| + if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"] + name = option.name.to_sym + case name + when :port + name = :Port + when :binding + name = :Host + when :dev_caching + name = :caching + when :daemonize + name = :daemon + end + user_supplied_options << name + end + end + user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"] + user_supplied_options << :Port if ENV["PORT"] + user_supplied_options.uniq + end + end + + def port + options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i + end + + def host + if options[:binding] + options[:binding] + else + default_host = environment == "development" ? "localhost" : "0.0.0.0" + + if ENV["HOST"] && !ENV["BINDING"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using the `HOST` environment variable to specify the IP is deprecated and will be removed in Rails 6.1. + Please use `BINDING` environment variable instead. + MSG + + return ENV["HOST"] + end + + ENV.fetch("BINDING", default_host) + end + end + + def environment + options[:environment] || Rails::Command.environment + end + + def restart_command + "bin/rails server #{@original_options.join(" ")} --restart" + end + + def early_hints + options[:early_hints] + end + + def log_to_stdout? + options.fetch(:log_to_stdout) do + options[:daemon].blank? && environment == "development" + end + end + + def pid + File.expand_path(options[:pid]) + end + + def self.banner(*) + "rails server -u [thin/puma/webrick] [options]" + end + + def prepare_restart + FileUtils.rm_f(options[:pid]) if options[:restart] + end + + def deprecate_positional_rack_server_and_rewrite_to_option(original_options) + if using + ActiveSupport::Deprecation.warn(<<~MSG.squish) + Passing the Rack server name as a regular argument is deprecated + and will be removed in the next Rails version. Please, use the -u + option instead. + MSG + + original_options.concat [ "-u", using ] + else + # Use positional internally to get around Thor's immutable options. + # TODO: Replace `using` occurrences with `options[:using]` after deprecation removal. + @using = options[:using] + end + end + + def rack_server_suggestion(server) + if server.in?(RACK_SERVERS) + <<~MSG + Could not load server "#{server}". Maybe you need to the add it to the Gemfile? + + gem "#{server}" + + Run `rails server --help` for more options. + MSG + else + suggestion = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) + suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion + + <<~MSG + Could not find server "#{server}". #{suggestion_msg} + Run `rails server --help` for more options. + MSG + end + end + + def print_boot_information(server, url) + say <<~MSG + => Booting #{ActiveSupport::Inflector.demodulize(server)} + => Rails #{Rails.version} application starting in #{Rails.env} #{url} + => Run `rails server --help` for more startup options + MSG + end + end + end +end diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb deleted file mode 100644 index dd069f081f..0000000000 --- a/railties/lib/rails/commands/test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require "rails/test_unit/minitest_plugin" - -if defined?(ENGINE_ROOT) - $: << File.expand_path('test', ENGINE_ROOT) -else - $: << File.expand_path('../../test', APP_PATH) -end - -exit Minitest.run(ARGV) diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb new file mode 100644 index 0000000000..00ea9ac4a6 --- /dev/null +++ b/railties/lib/rails/commands/test/test_command.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/command" +require "rails/test_unit/runner" +require "rails/test_unit/reporter" + +module Rails + module Command + class TestCommand < Base # :nodoc: + no_commands do + def help + say "Usage: #{Rails::TestUnitReporter.executable} [options] [files or directories]" + say "" + say "You can run a single test by appending a line number to a filename:" + say "" + say " #{Rails::TestUnitReporter.executable} test/models/user_test.rb:27" + say "" + say "You can run multiple files and directories at the same time:" + say "" + say " #{Rails::TestUnitReporter.executable} test/controllers test/integration/login_test.rb" + say "" + say "By default test failures and errors are reported inline during a run." + say "" + + Minitest.run(%w(--help)) + end + end + + def perform(*) + $LOAD_PATH << Rails::Command.root.join("test").to_s + + Rails::TestUnit::Runner.parse_options(ARGV) + Rails::TestUnit::Runner.run(ARGV) + end + end + end +end diff --git a/railties/lib/rails/commands/version/version_command.rb b/railties/lib/rails/commands/version/version_command.rb new file mode 100644 index 0000000000..3e2112b6d4 --- /dev/null +++ b/railties/lib/rails/commands/version/version_command.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Rails + module Command + class VersionCommand < Base # :nodoc: + def perform + Rails::Command.invoke :application, [ "--version" ] + end + end + end +end |