{{< details >}}

  • Tier: 基础版, 专业版, 旗舰版
  • Offering: 私有化部署

{{< /details >}}

在极狐GitLab的核心是一个 使用 Ruby on Rails 框架构建的网络应用程序。Rails 控制台提供了一种从命令行与极狐GitLab实例交互的方法,并授予对 Rails 内置的优秀工具的访问权限。

{{< alert type=”warning” >}}

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

{{< /alert >}}

Rails 控制台适用于正在排查问题或需要检索只能通过直接访问极狐GitLab应用程序才能完成的数据的极狐GitLab系统管理员。需要基本的 Ruby 知识(可以尝试 这个 30 分钟的教程 进行快速入门)。Rails 经验有用但不是必需的。

启动 Rails 控制台会话

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

{{< tabs >}}

{{< tab title=”Linux package (Omnibus)” >}}

sudo gitlab-rails console

{{< /tab >}}

{{< tab title=”Docker” >}}

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

{{< /tab >}}

{{< tab title=”Self-compiled (source)” >}}

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

{{< /tab >}}

{{< tab title=”Helm chart (Kubernetes)” >}}

# find the pod
kubectl get pods --namespace <namespace> -lapp=toolbox

# open the Rails console
kubectl exec -it -c toolbox <toolbox-pod-name> -- gitlab-rails console

{{< /tab >}}

{{< /tabs >}}

要退出控制台,请输入: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

属性

查看可用的属性,使用漂亮打印(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 语句超时。

在极狐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"
    
    # 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

计时操作

如果您想要计时一个或多个操作,请使用以下格式,用您选择的 Ruby 或 Rails 命令替换占位符 <operation>

# 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(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 模型查询数据库

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 验证。验证是在应用程序级别设置的业务逻辑,以防止不希望的数据被保存到数据库中,并且在大多数情况下会附带有用的消息,告诉您如何修复问题输入。

我们也可以将叹号(Ruby 术语为 !)添加到 .update 中:

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

在 Ruby 中,方法名以 ! 结尾的通常被称为“叹号方法”。按照惯例,叹号表示方法直接修改其作用对象,而不是返回转换结果并保持底层对象不变。对于写入数据库的 Active Record 方法,叹号方法还具有额外功能:在发生错误时引发显式异常,而不是仅返回 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)

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

验证错误会阻止整个对象被保存到数据库中。您可以在下面的部分中看到一些内容。如果您在极狐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: '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')

# 获取群组的父群组
group.parent

# 获取群组的子群组
group.children

获取群组的项目:

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

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

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

获取 CI 流水线或构建:

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

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

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

ApplicationSetting.current

irb 中打开对象

{{< alert type=”warning” >}}

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

{{< /alert >}}

有时在对象的上下文中执行方法更容易。您可以在 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

过滤控制台输出

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

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

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

> Plan.default.limits.ci_instance_level_variables
=> 25