Rails 控制台

Rails 控制台提供了一种使用命令行与极狐GitLab 实例交互的方法,并且还允许访问 Rails 中内置的工具。

caution Rails 控制台直接与极狐GitLab 交互。在许多情况下,没有机制可以阻止您永久修改、破坏或破坏生产数据。如果您想在没有任何后果的情况下探索 Rails 控制台,强烈建议您在测试环境中进行。

Rails 控制台适用于正在对问题进行故障排除,或需要检索某些只能通过直接访问极狐GitLab 应用程序来完成的数据的系统管理员。

此功能需要 Ruby 的基本知识(尝试 30 分钟教程的快速介绍)。Rails 经验很有用,但不是必需的。

启动 Rails 控制台会话

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

::Tabs

:::TabTitle Linux 软件包 (Omnibus)

sudo gitlab-rails console

:::TabTitle Docker

docker exec -it <container-id> gitlab-rails console

:::TabTitle 自编译(源)

sudo -u git -H bundle exec rails console -e production

:::TabTitle Helm chart (Kubernetes)

控制台位于 toolbox pod。

::EndTabs

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

启用 Active Record 日志记录

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

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

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

ActiveRecord::Base.logger = nil

属性

查看可用属性,使用漂亮打印(pp)格式化。

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

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

部分输出:

{"id"=>1234,
 "email"=>"someuser@example.com",
 "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

属性

查看可用属性,使用优雅打印 (pp) 格式化。

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

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

部分输出:

{"id"=>1234,
 "email"=>"someuser@example.com",
 "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 语句超时:

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

此更改仅影响当前 Rails 控制台会话,不会保留在极狐GitLab 生产环境或下一个 Rails 控制台会话中。

输出 Rails 控制台会话历史记录

在 rails 控制台输入以下命令,显示您的命令历史记录。

puts Readline::HISTORY.to_a

然后,您可以将其复制到剪贴板并保存以备将来参考。

使用 Rails Runner

如果您需要在极狐GitLab 生产环境的上下文中运行一些 Ruby 代码,您可以使用 Rails Runner 来完成。 执行脚本文件时,该脚本必须可由 git 用户访问。

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

  • Linux 软件包安装:

    sudo gitlab-rails runner "RAILS_COMMAND"
    
    # Example with a two-line Ruby script
    sudo gitlab-rails runner "user = User.first; puts user.username"
    
    # Example with a ruby script file (make sure to use the full path)
    sudo gitlab-rails runner /path/to/script.rb
    
  • 自编译安装:

    sudo -u git -H bundle exec rails runner -e production "RAILS_COMMAND"
    
    # Example with a two-line Ruby script
    sudo -u git -H bundle exec rails runner -e production "user = User.first; puts user.username"
    
    # Example with a ruby script file (make sure to use the full path)
    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? "sing" }
Array.methods.grep(/sing/)

查找方法源

instance_of_object.method(:foo).source_location

# Example for when we would call 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 命令:

# A single operation
Benchmark.measure { <operation> }

# A breakdown of multiple operations
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(email: 'admin@example.com')
  • 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%')

# for example to query the projects
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: 'hunter2@example.com')

现在,让我们尝试一些不同的东西:

# Retrieve the object again so we get its latest state
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 Validation。 验证是在应用程序级别放置的业务逻辑,以防止将不需要的数据保存到数据库中,并且在大多数情况下会附带有用的消息,让您知道如何解决问题输入。

我们还可以在 .update 中添加 bang(Ruby 中的 !):

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

在 Ruby 中,以 ! 结尾的方法名称通常被称为 “bang 方法”。按照惯例,bang 表示该方法直接修改它正在作用的对象,而不是返回转换后的结果并保持底层对象不变。对于写入数据库的 Active Record 方法,bang 方法还有一个附加功能:只要发生错误,它们就会引发显式异常,而不仅仅是返回 false

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

# Retrieve the object again so we get its latest state
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save!(validate: false)

不建议这样做,因为通常会进行验证以确保用户提供的数据的完整性和一致性。

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

与 Active Record 对象交互

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

例如,开发人员添加了一些有助于双重身份验证的方法:

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.u2f_registrations.destroy_all # rubocop: disable DestroyAll
  end
end

def two_factor_enabled?
  two_factor_otp_enabled? || two_factor_u2f_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 返回!让我们像之前一样通过添加 bang 来了解发生了什么:

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: 'admin@example.com')
User.find_by(username: 'root')

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

User.find_by_any_email('user@example.com')

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 获取项目的议题或合并请求:

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

iid 的意思是“内部 ID”,这是我们将议题和合并请求 ID 限制在每个极狐GitLab 项目范围内的方式。

通过其路径获取一个群组:

Group.find_by_full_path('group/subgroup')

获取群组的相关群组:

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

# Get a group's parent group
group.parent

# Get a group's child groups
group.children

获取群组的项目:

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

# Get group's immediate child projects
group.projects

# Get group's child projects, including those in subgroups
group.all_projects

获取 CI 流水线或构建:

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

流水线和作业 ID 号在您的极狐GitLab 实例中全局递增,因此无需使用内部 ID 属性来查找它们,这与议题或合并请求不同。

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

ApplicationSetting.current

irb 中打开对象

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

有时,如果您处于对象的上下文中,则通过方法会更容易。您可以填充到 Object 的命名空间中,以便在任何对象的上下文中打开 irb

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

project = Project.last
# => #<Project id:2537 root/discard>>
project.irb
# Notice new context
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

过滤控制台输出

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

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

要解决过滤问题,请直接从对象读取值。例如:

> Plan.default.limits.ci_instance_level_variables
=> 25