Help us learn about your current experience with the documentation. Take the survey.

Rails 控制台

  • Tier: Free, Premium, Ultimate
  • Offering: GitLab Self-Managed

GitLab 的核心是一个使用 Ruby on Rails 框架构建的 Web 应用程序 (了解更多)Rails 控制台 提供了一种从命令行与 GitLab 实例交互的方式,同时也让你能够访问 Rails 内置的强大工具。

Rails 控制台直接与 GitLab 交互。在许多情况下, 没有防护措施可以防止你永久修改、损坏或销毁生产数据。如果你想无后果地探索 Rails 控制台, 强烈建议在测试环境中进行。

Rails 控制台面向正在排查问题或需要通过直接访问 GitLab 应用程序 来获取某些数据的 GitLab 系统管理员。需要基本的 Ruby 知识(可以尝试 这个 30 分钟教程快速入门)。 有 Rails 经验会很有帮助,但不是必需的。

启动 Rails 控制台会话

启动 Rails 控制台会话的过程取决于 GitLab 安装类型。

sudo gitlab-rails console
docker exec -it <container-id> gitlab-rails console
sudo -u git -H bundle exec rails console -e production
# 查找 pod
kubectl get pods --namespace <namespace> -lapp=toolbox

# 打开 Rails 控制台
kubectl exec -it -c toolbox <toolbox-pod-name> -- gitlab-rails console

要退出控制台,请输入:quit

禁用自动补全

Ruby 自动补全会降低终端速度。如果你想:

  • 禁用自动补全,运行 Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE] = false
  • 重新启用自动补全,运行 Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE] = true

启用 Active Record 日志记录

你可以在 Rails 控制台会话中启用 Active Record 调试日志输出, 通过运行以下命令:

ActiveRecord::Base.logger = Logger.new($stdout)

默认情况下,上面的脚本会将日志输出到标准输出。你可以通过将 $stdout 替换为所需的文件路径, 来指定将输出重定向到日志文件。例如,下面的代码会将所有内容记录到 /tmp/output.log

ActiveRecord::Base.logger = Logger.new('/tmp/output.log')

这显示了你在控制台中运行的任何 Ruby 代码触发的数据库查询信息。要再次关闭日志记录,请运行:

ActiveRecord::Base.logger = nil

属性

查看可用的属性,使用 pretty print (pp) 格式化显示。

例如,确定哪些属性包含用户的姓名和电子邮件地址:

u = User.find_by_username('someuser')
pp u.attributes

部分输出:

{"id"=>1234,
 "email"=>"[email protected]",
 "sign_in_count"=>99,
 "name"=>"S User",
 "username"=>"someuser",
 "first_name"=>nil,
 "last_name"=>nil,
 "bot_type"=>nil}

然后使用这些属性,例如测试 SMTP

e = u.email
n = u.name
Notify.test_email(e, "Test email for #{n}", 'Test email').deliver_now
#
Notify.test_email(u.email, "Test email for #{u.name}", 'Test email').deliver_now

禁用数据库语句超时

你可以为当前的 Rails 控制台会话禁用 PostgreSQL 语句超时。

在 GitLab 15.11 及更早版本中,要禁用数据库语句超时,请运行:

ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')

在 GitLab 16.0 及更高版本中,GitLab 默认使用两个数据库连接。要禁用数据库语句超时,请运行:

ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')
Ci::ApplicationRecord.connection.execute('SET statement_timeout TO 0')

运行 GitLab 16.0 及更高版本但重新配置为使用单个数据库连接的实例,应使用 GitLab 15.11 及更早版本的代码来禁用数据库语句超时。

禁用数据库语句超时仅影响当前的 Rails 控制台会话,不会持久化到 GitLab 生产环境或下一个 Rails 控制台会话中。

输出 Rails 控制台会话历史

在 rails 控制台中输入以下命令来显示你的命令历史。

puts Reline::HISTORY.to_a

然后你可以将其复制到剪贴板并保存以供将来参考。

使用 Rails Runner

如果你需要在 GitLab 生产环境的上下文中运行一些 Ruby 代码, 你可以使用 Rails Runner 来实现。 当执行脚本文件时,脚本必须对 git 用户可访问。

当命令或脚本完成时,Rails Runner 进程结束。 例如,在其他脚本或 cron 作业中运行时很有用。

  • 对于 Linux 包安装:

    sudo gitlab-rails runner "RAILS_COMMAND"
    
    # 使用两行 Ruby 脚本的示例
    sudo gitlab-rails runner "user = User.first; puts user.username"
    
    # 使用 ruby 脚本文件的示例(请确保使用完整路径)
    sudo gitlab-rails runner /path/to/script.rb
  • 对于自编译安装:

    sudo -u git -H bundle exec rails runner -e production "RAILS_COMMAND"
    
    # 使用两行 Ruby 脚本的示例
    sudo -u git -H bundle exec rails runner -e production "user = User.first; puts user.username"
    
    # 使用 ruby 脚本文件的示例(请确保使用完整路径)
    sudo -u git -H bundle exec rails runner -e production /path/to/script.rb

Rails Runner 不会产生与控制台相同的输出。

如果你在控制台上设置了一个变量,控制台会产生有用的调试输出, 例如变量内容或引用实体的属性:

irb(main):001:0> user = User.first
=> #<User id:1 @root>

Rails Runner 不会这样做:你必须明确地生成输出:

$ sudo gitlab-rails runner "user = User.first"
$ sudo gitlab-rails runner "user = User.first; puts user.username ; puts user.id"
root
1

基本的 Ruby 知识非常有用。尝试 这个 30 分钟教程快速入门。 Rails 经验有帮助但不是必需的。

查找对象的具体方法

Array.methods.select { |m| m.to_s.include? "ing" }
Array.methods.grep(/ing/)

查找方法源码

instance_of_object.method(:foo).source_location

# 例如当我们调用 project.private? 时
project.method(:private?).source_location

限制输出

在语句末尾添加分号(;)和后续语句可以防止默认的隐式返回输出。如果你已经在明确打印详细信息并且可能有大量返回输出时,这很有用:

puts ActiveRecord::Base.descendants; :ok
Project.select(&:pages_deployed?).each {|p| puts p.path }; true

获取或存储上次操作的结果

下划线(_)表示前一个语句的隐式返回。你可以用它来快速从前一个命令的输出中分配一个变量:

Project.last
# => #<Project id:2537 root/discard>>
project = _
# => #<Project id:2537 root/discard>>
project.id
# => 2537

计时操作

如果你想对一个或多个操作进行计时,请使用以下格式,将占位符 <operation> 替换为你选择的 Ruby 或 Rails 命令:

# 单个操作
Benchmark.measure { <operation> }

# 多个操作的详细分解
Benchmark.bm do |x|
  x.report(:label1) { <operation_1> }
  x.report(:label2) { <operation_2> }
end

有关更多信息,请查看我们关于基准测试的开发者文档。

Active Record 对象

查找数据库持久化对象

在底层,Rails 使用 Active Record, 这是一个对象关系映射系统,用于读取、写入和将应用程序对象映射到 PostgreSQL 数据库。 这些映射由 Active Record 模型处理,这些模型是在 Rails 应用中定义的 Ruby 类。 对于 GitLab,模型类可以在 /opt/gitlab/embedded/service/gitlab-rails/app/models 中找到。

让我们为 Active Record 启用调试日志,以便我们可以看到底层的数据库查询:

ActiveRecord::Base.logger = Logger.new($stdout)

现在,让我们尝试从数据库中检索一个用户:

user = User.find(1)

这将返回:

D, [2020-03-05T16:46:25.571238 #910] DEBUG -- :   User Load (1.8ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
=> #<User id:1 @root>

我们可以看到我们查询了数据库中的 users 表,查找 id 列值为 1 的行, 而 Active Record 已将该数据库记录转换为一个我们可以与之交互的 Ruby 对象。尝试以下操作:

  • user.username
  • user.created_at
  • user.admin

按照惯例,列名直接转换为 Ruby 对象属性, 所以你应该能够通过 user.<column_name> 来查看属性的值。

同样按照惯例,Active Record 类名(单数形式,驼峰命名)直接映射到表名(复数形式,下划线命名), 反之亦然。例如,users 表映射到 User 类,而 application_settings 表映射到 ApplicationSetting 类。

你可以在 Rails 数据库架构中找到表名和列名的列表, 该文件位于 /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb

你也可以通过属性名从数据库中查找对象:

user = User.find_by(username: 'root')

这将返回:

D, [2020-03-05T17:03:24.696493 #910] DEBUG -- :   User Load (2.1ms)  SELECT "users".* FROM "users" WHERE "users"."username" = 'root' LIMIT 1
=> #<User id:1 @root>

尝试以下操作:

  • User.find_by(username: 'root')
  • User.where.not(admin: true)
  • User.where('created_at < ?', 7.days.ago)

你注意到最后两个命令返回了一个 ActiveRecord::Relation 对象, 它似乎包含多个 User 对象吗?

到目前为止,我们一直在使用 .find.find_by,这些方法设计为只返回单个对象 (注意生成的 SQL 查询中的 LIMIT 1?)。 .where 用于当需要获取对象集合时。

让我们获取一个非管理员用户的集合,看看我们能用它做什么:

users = User.where.not(admin: true)

这将返回:

D, [2020-03-05T17:11:16.845387 #910] DEBUG -- :   User Load (2.8ms)  SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE LIMIT 11
=> #<ActiveRecord::Relation [#<User id:3 @support-bot>, #<User id:7 @alert-bot>, #<User id:5 @carrie>, #<User id:4 @bernice>, #<User id:2 @anne>]>

现在,尝试以下操作:

  • users.count
  • users.order(created_at: :desc)
  • users.where(username: 'support-bot')

在最后一个命令中,我们看到我们可以链式调用 .where 语句来生成更复杂的查询。 还请注意,虽然返回的集合只包含单个对象,但我们不能直接与它交互:

users.where(username: 'support-bot').username

这将返回:

Traceback (most recent call last):
        1: from (irb):37
D, [2020-03-05T17:18:25.637607 #910] DEBUG -- :   User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE AND "users"."username" = 'support-bot' LIMIT 11
NoMethodError (undefined method `username' for #<ActiveRecord::Relation [#<User id:3 @support-bot>]>)
Did you mean?  by_username

让我们使用 .first 方法从集合中检索单个对象,获取集合中的第一项:

users.where(username: 'support-bot').first.username

我们现在得到了想要的结果:

D, [2020-03-05T17:18:30.406047 #910] DEBUG -- :   User Load (2.6ms)  SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE AND "users"."username" = 'support-bot' ORDER BY "users"."id" ASC LIMIT 1
=> "support-bot"

有关使用 Active Record 从数据库检索数据的不同方法的更多信息, 请参阅 Active Record 查询接口文档

使用 Active Record 模型查询数据库

m = Model.where('attribute like ?', 'ex%')

# 例如查询项目
projects = Project.where('path like ?', 'Oumua%')

修改 Active Record 对象

在上一节中,我们学习了如何使用 Active Record 检索数据库记录。 现在,让我们学习如何将更改写入数据库。

首先,让我们检索 root 用户:

user = User.find_by(username: 'root')

接下来,让我们尝试更新用户的密码:

user.password = 'password'
user.save

这将返回:

Enqueued ActionMailer::MailDeliveryJob (Job ID: 05915c4e-c849-4e14-80bb-696d5ae22065) to Sidekiq(mailers) with arguments: "DeviseMailer", "password_change", "deliver_now", #<GlobalID:0x00007f42d8ccebe8 @uri=#<URI::GID gid://gitlab/User/1>>
=> true

在这里,我们看到 .save 命令返回了 true,表示密码更改已成功保存到数据库。

我们还看到保存操作触发了一些其他操作——在这种情况下是发送电子邮件通知的后台作业。 这是一个 Active Record 回调 的示例 ——代码被设计为在 Active Record 对象生命周期中的事件发生时运行。 这也是为什么当需要直接更改数据时优先使用 Rails 控制台的原因, 因为通过直接数据库查询进行的更改不会触发这些回调。

也可以在一行中更新属性:

user.update(password: 'password')

或者一次更新多个属性:

user.update(password: 'password', email: '[email protected]')

现在,让我们尝试不同的操作:

# 再次检索对象以获取其最新状态
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save

这返回 false,表示我们所做的更改没有保存到数据库。 你可能已经猜到了原因,但让我们确认一下:

user.save!

这应该返回:

Traceback (most recent call last):
        1: from (irb):64
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password)

啊!我们触发了 Active Record 验证。 验证是在应用程序级别设置的业务逻辑,用于防止不想要的数据被保存到数据库, 并且在大多数情况下会提供有用的消息告诉你如何修复有问题的输入。

我们还可以在 .update 后添加感叹号(Ruby 中 ! 的说法):

user.update!(password: 'password', password_confirmation: 'hunter2')

在 Ruby 中,以 ! 结尾的方法名通常被称为"bang 方法"。按照惯例, 感叹号表示该方法直接修改它所作用的对象,而不是返回转换后的结果并保持底层对象不变。 对于写入数据库的 Active Record 方法,bang 方法还具有另一个功能: 当发生错误时,它们会抛出明确的异常,而不仅仅是返回 false

我们也可以完全跳过验证:

# 再次检索对象以获取其最新状态
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save!(validate: false)

这不推荐,因为验证通常是为了确保用户提供的数据的完整性和一致性。

验证错误会阻止整个对象被保存到数据库。 你可以在下面的部分看到这一点。如果你在提交表单时在 GitLab UI 中看到神秘的红色横幅, 这通常是快速找到问题根源的最快方法。

与 Active Record 对象交互

归根结底,Active Record 对象只是标准的 Ruby 对象。因此, 我们可以在它们上面定义执行任意操作的方法。

例如,GitLab 开发人员添加了一些帮助双因素认证的方法:

def disable_two_factor!
  transaction do
    update(
      otp_required_for_login:      false,
      encrypted_otp_secret:        nil,
      encrypted_otp_secret_iv:     nil,
      encrypted_otp_secret_salt:   nil,
      otp_grace_period_started_at: nil,
      otp_backup_codes:            nil
    )
    self.webauthn_registrations.destroy_all # rubocop: disable DestroyAll
  end
end

def two_factor_enabled?
  two_factor_otp_enabled? || two_factor_webauthn_enabled?
end

(参见:/opt/gitlab/embedded/service/gitlab-rails/app/models/user.rb

然后我们可以在任何用户对象上使用这些方法:

user = User.find_by(username: 'root')
user.two_factor_enabled?
user.disable_two_factor!

一些方法是由 GitLab 使用的 gem 或 Ruby 软件包定义的。 例如,GitLab 用于管理用户状态的 StateMachines gem:

state_machine :state, initial: :active do
  event :block do

  ...

  event :activate do

  ...

end

尝试一下:

user = User.find_by(username: 'root')
user.state
user.block
user.state
user.activate
user.state

早些时候,我们提到验证错误会阻止整个对象被保存到数据库。 让我们看看这如何可能导致意外的交互:

user.password = 'password'
user.password_confirmation = 'hunter2'
user.block

我们得到 false 返回!让我们像之前那样添加一个感叹号来看看发生了什么:

user.block!

这将返回:

Traceback (most recent call last):
        1: from (irb):87
StateMachines::InvalidTransition (Cannot transition state via :block from :active (Reason(s): Password confirmation doesn't match Password))

我们看到,当我们尝试以任何方式更新用户时, 来自看似完全独立属性的验证错误会回来困扰我们。

在实际操作中,我们有时会看到这种情况发生在 GitLab 管理设置上—— 验证有时会在 GitLab 更新中被添加或更改,导致之前保存的设置现在验证失败。 因为你只能通过 UI 一次更新一部分设置,在这种情况下, 恢复到良好状态的唯一方法是通过 Rails 控制台直接操作。

常用的 Active Record 模型及如何查找对象

通过主电子邮件地址或用户名获取用户

User.find_by(email: '[email protected]')
User.find_by(username: 'root')

通过主或次电子邮件地址获取用户

User.find_by_any_email('[email protected]')

find_by_any_email 方法是 GitLab 开发人员添加的自定义方法, 而不是 Rails 提供的默认方法。

获取管理员用户的集合

User.admins

admins 是一个范围便捷方法, 它在底层执行 where(admin: true)

通过路径获取项目

Project.find_by_full_path('group/subgroup/project')

find_by_full_path 是 GitLab 开发人员添加的自定义方法, 而不是 Rails 提供的默认方法。

通过数字 ID 获取项目的 issue 或 merge request

project = Project.find_by_full_path('group/subgroup/project')
project.issues.find_by(iid: 42)
project.merge_requests.find_by(iid: 42)

iid 表示"内部 ID",这就是我们如何将 issue 和 merge request ID 限制在每个 GitLab 项目范围内。

通过路径获取组

Group.find_by_full_path('group/subgroup')

获取组的关联组

group = Group.find_by_full_path('group/subgroup')

# 获取组的父组
group.parent

# 获取组的子组
group.children

获取组的项目

group = Group.find_by_full_path('group/subgroup')

# 获取组的直接子项目
group.projects

# 获取组的子项目,包括子组中的项目
group.all_projects

获取 CI pipeline 或构建

Ci::Pipeline.find(4151)
Ci::Build.find(66124)

pipeline 和作业 ID 号在你的 GitLab 实例中全局递增, 所以不需要使用内部 ID 属性来查找它们,这与 issue 或 merge request 不同。

获取当前应用程序设置对象

ApplicationSetting.current

irb 中打开对象

如果命令未正确运行或在正确的条件下运行,更改数据的命令可能会造成损害。始终首先在测试环境中运行命令,并准备好备份实例以进行恢复。

有时,如果你在对象的上下文中,通过方法会更简单。你可以 shim 到 Object 的命名空间中, 让你可以在任何对象的上下文中打开 irb

Object.define_method(:irb) { binding.irb }

project = Project.last
# => #<Project id:2537 root/discard>>
project.irb
# 注意新的上下文
irb(#<Project>)> web_url
# => "https://gitlab-example/root/discard"

故障排除

Rails Runner 语法错误

gitlab-rails 命令默认使用非 root 账户和组执行 Rails Runner:git:git

如果非 root 账户无法找到传递给 gitlab-rails runner 的 Ruby 脚本文件名, 你可能会得到语法错误,而不是文件无法访问的错误。

一个常见的原因是脚本被放在了 root 账户的主目录中。

runner 尝试将路径和文件参数解析为 Ruby 代码。

例如:

[root ~]# echo 'puts "hello world"' > ./helloworld.rb
[root ~]# sudo gitlab-rails runner ./helloworld.rb
Please specify a valid ruby command or the path of a script to run.
Run 'rails runner -h' for help.

/opt/gitlab/..../runner_command.rb:45: syntax error, unexpected '.'
./helloworld.rb
^
[root ~]# sudo gitlab-rails runner /root/helloworld.rb
Please specify a valid ruby command or the path of a script to run.
Run 'rails runner -h' for help.

/opt/gitlab/..../runner_command.rb:45: unknown regexp options - hllwrld
[root ~]# mv ~/helloworld.rb /tmp
[root ~]# sudo gitlab-rails runner /tmp/helloworld.rb
hello world

如果目录可以访问但文件无法访问,应该生成有意义的错误:

[root ~]# chmod 400 /tmp/helloworld.rb
[root ~]# sudo gitlab-rails runner /tmp/helloworld.rb
Traceback (most recent call last):
      [traceback removed]
/opt/gitlab/..../runner_command.rb:42:in `load': cannot load such file -- /tmp/helloworld.rb (LoadError)

如果你遇到类似的错误:

[root ~]# sudo gitlab-rails runner helloworld.rb
Please specify a valid ruby command or the path of a script to run.
Run 'rails runner -h' for help.

undefined local variable or method `helloworld' for main:Object

你可以将文件移动到 /tmp 目录,或者创建一个由用户 git 拥有的新目录并将脚本保存在该目录中,如下所示:

sudo mkdir /scripts
sudo mv /script_path/helloworld.rb /scripts
sudo chown -R git:git /scripts
sudo chmod 700 /scripts
sudo gitlab-rails runner /scripts/helloworld.rb

过滤的控制台输出

控制台中的某些输出可能会被默认过滤,以防止泄露某些值, 如变量、日志或密钥。此输出显示为 [FILTERED]。例如:

> Plan.default.actual_limits
=> ci_instance_level_variables: "[FILTERED]",

要绕过过滤,直接从对象中读取值。例如:

> Plan.default.limits.ci_instance_level_variables
=> 25