Railsコンソール

GitLabの中心には、Ruby on Railsフレームワークを使って構築されたWebアプリケーションがあります。Railsコンソールは、コマンドラインからGitLabインスタンスと対話する方法を提供し、Railsに組み込まれた素晴らしいツールにもアクセスできます。

caution
RailsコンソールはGitLabと直接対話します。多くの場合、本番データの永久的な変更、破損、破壊を防ぐ手すりはありません。Railsコンソールを無難に探検したい場合は、テスト環境で行うことを強くお勧めします。

Railsコンソールは、GitLabシステム管理者が問題のトラブルシューティングをしたり、GitLabアプリケーションに直接アクセスしなければできないようなデータを取得したりするためのものです。Rubyの基本的な知識が必要です(30分のチュートリアルで簡単に紹介されています)。Rails の経験があると便利ですが、必須ではありません。

Rails コンソールセッションの開始

Railsのコンソールセッションを開始する手順は、GitLabのインストールの種類によって異なります。

Linux package (Omnibus)
sudo gitlab-rails console
Docker
docker exec -it <container-id> gitlab-rails console
Self-compiled (source)
sudo -u git -H bundle exec rails console -e production
Helm chart (Kubernetes)

コンソールは toolbox ポッドにあります。詳しくはKubernetesチートシートを参照してください。

コンソールを終了するには、次のように入力します:quit.

アクティブレコードログの有効化

を実行すると、RailsコンソールセッションでActive Recordのデバッグロギングの出力を有効にできます:

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

これにより、コンソールで実行したRubyコードによってトリガされたデータベースクエリの情報が表示されます。ロギングを再びオフにするには、以下を実行します:

ActiveRecord::Base.logger = nil

属性

pretty print (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

出力の制限

ステートメントの最後にセミコロン(;)とフォローアップ・ステートメントを追加すると、デフォルトの暗黙のリターン出力を防ぐことができます。これは、すでに詳細を明示的に出力していて、return出力が多くなる可能性がある場合に使用できます:

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

オペレーション時間

1つまたは複数のオペレーションを時間指定したい場合は、プレースホルダ<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

詳細については、ベンチマークに関する開発者向けドキュメントをレビューしてください。

アクティブレコードオブジェクト

データベースが保持するオブジェクトの検索

RailsではオブジェクトリレーショナルマッピングシステムであるActive Recordを使用して、アプリケーションオブジェクトのPostgreSQLデータベースへの読み書きとマッピングを行います。これらのマッピングはActive Recordモデルによって処理されます。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)

最後の2つのコマンドは、複数のUser オブジェクトを含んでいるように見えるActiveRecord::Relation オブジェクトを返したことに気づきましたか?

これまでは、.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 ステートメントを連結して、より複雑なクエリを生成できることがわかります。また、返されたコレクションにはオブジェクトが1つだけ含まれており、直接操作することはできません:

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 Query Interface のドキュメントを参照してください。

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コンソールを使用するのが望ましい理由もここにあります。

1行で属性を更新することも可能です:

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)

アクティブレコードのバリデーションに引っかかりました。バリデーションは、不要なデータがデータベースに保存されるのを防ぐためにアプリケーションレベルで実行されるビジネスロジックで、ほとんどの場合、問題の入力を修正する方法を知らせる有用なメッセージが表示されます。

また、.update に bang (Ruby speak for!) を追加することもできます:

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)

バリデーションは通常、ユーザーが提供したデータの整合性と一貫性を保証するために行われるものだからです。

バリデーションエラーが発生すると、オブジェクト全体がデータベースに保存されなくなります。これは下のセクションで少し見ることができます。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がユーザーの状態を管理するために使っているStateMachinesgem:

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 メソッドは、Rails が提供するデフォルトのメソッドではなく、GitLab 開発者が追加したカスタムメソッドです。

管理者ユーザーのコレクションを取得します:

User.admins

adminsスコープの便宜メソッドでwhere(admin: true) を内部で実行します。

プロジェクトをパスで取得します:

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

find_by_full_path はRailsが提供するデフォルトメソッドではなく、GitLab開発者が追加したカスタムメソッドです。

プロジェクトのイシューやマージリクエストを数値 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ランナーsyntax error

gitlab-rails コマンドは Rails Runner を非 root アカウントとグループを使用して実行します(デフォルト:git:git )。

非 root アカウントでgitlab-rails runner に渡された Ruby スクリプトのファイル名が見つからない場合、ファイルにアクセスできなかったというエラーではなく、構文エラーが表示されることがあります。

よくある原因は、スクリプトがルートアカウントのホームディレクトリに置かれていることです。

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