Navigating GitLab via Rails console

At the heart of GitLab is a web application built using the Ruby on Rails framework. Thanks to this, we also get access to the amazing tools built right into Rails. This guide introduces the Rails console and the basics of interacting with your GitLab instance from the command line.

caution
The Rails console interacts directly with your GitLab instance. In many cases, there are no handrails to prevent you from permanently modifying, corrupting or destroying production data. If you would like to explore the Rails console with no consequences, you are strongly advised to do so in a test environment.

This guide is targeted at GitLab system administrators who are troubleshooting a problem or must retrieve some data that can only be done through direct access of the GitLab application. Basic knowledge of Ruby is needed (try this 30-minute tutorial for a quick introduction). Rails experience is helpful to have but not a must.

Starting a Rails console session

Your type of GitLab installation determines how to start a rails console.

The following code examples take place inside the Rails console and also assume an Omnibus GitLab installation.

Active Record objects

Looking up database-persisted objects

Under the hood, Rails uses Active Record, an object-relational mapping system, to read, write, and map application objects to the PostgreSQL database. These mappings are handled by Active Record models, which are Ruby classes defined in a Rails app. For GitLab, the model classes can be found at /opt/gitlab/embedded/service/gitlab-rails/app/models.

Let’s enable debug logging for Active Record so we can see the underlying database queries made:

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

Now, let’s try retrieving a user from the database:

user = User.find(1)

Which would return:

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>

We can see that we’ve queried the users table in the database for a row whose id column has the value 1, and Active Record has translated that database record into a Ruby object that we can interact with. Try some of the following:

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

By convention, column names are directly translated into Ruby object attributes, so you should be able to do user.<column_name> to view the attribute’s value.

Also by convention, Active Record class names (singular and in camel case) map directly onto table names (plural and in snake case) and vice versa. For example, the users table maps to the User class, while the application_settings table maps to the ApplicationSetting class.

You can find a list of tables and column names in the Rails database schema, available at /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.

You can also look up an object from the database by attribute name:

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

Which would return:

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>

Give the following a try:

  • User.find_by(email: 'admin@example.com')
  • User.where.not(admin: true)
  • User.where('created_at < ?', 7.days.ago)

Did you notice that the last two commands returned an ActiveRecord::Relation object that appeared to contain multiple User objects?

Up to now, we’ve been using .find or .find_by, which are designed to return only a single object (notice the LIMIT 1 in the generated SQL query?). .where is used when it is desirable to get a collection of objects.

Let’s get a collection of non-administrator users and see what we can do with it:

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

Which would return:

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>]>

Now, try the following:

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

In the last command, we see that we can chain .where statements to generate more complex queries. Notice also that while the collection returned contains only a single object, we cannot directly interact with it:

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

Which would return:

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

Let’s retrieve the single object from the collection by using the .first method to get the first item in the collection:

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

We now get the result we wanted:

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"

For more on different ways to retrieve data from the database using Active Record, please see the Active Record Query Interface documentation.

Modifying Active Record objects

In the previous section, we learned about retrieving database records using Active Record. Now, let’s learn how to write changes to the database.

First, let’s retrieve the root user:

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

Next, let’s try updating the user’s password:

user.password = 'password'
user.save

Which would return:

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

Here, we see that the .save command returned true, indicating that the password change was successfully saved to the database.

We also see that the save operation triggered some other action – in this case a background job to deliver an email notification. This is an example of an Active Record callback – code which is designated to run in response to events in the Active Record object life cycle. This is also why using the Rails console is preferred when direct changes to data is necessary as changes made via direct database queries do not trigger these callbacks.

It’s also possible to update attributes in a single line:

user.update(password: 'password')

Or update multiple attributes at once:

user.update(password: 'password', email: 'hunter2@example.com')

Now, let’s try something different:

# 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

This returns false, indicating that the changes we made were not saved to the database. You can probably guess why, but let’s find out for sure:

user.save!

This should return:

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

Aha! We’ve tripped an Active Record Validation. Validations are business logic put in place at the application-level to prevent unwanted data from being saved to the database and in most cases come with helpful messages letting you know how to fix the problem inputs.

We can also add the bang (Ruby speak for !) to .update:

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

In Ruby, method names ending with ! are commonly known as “bang methods”. By convention, the bang indicates that the method directly modifies the object it is acting on, as opposed to returning the transformed result and leaving the underlying object untouched. For Active Record methods that write to the database, bang methods also serve an additional function: they raise an explicit exception whenever an error occurs, instead of just returning false.

We can also skip validations entirely:

# 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)

This is not recommended, as validations are usually put in place to ensure the integrity and consistency of user-provided data.

A validation error prevents the entire object from being saved to the database. You can see a little of this in the section below. If you’re getting a mysterious red banner in the GitLab UI when submitting a form, this can often be the fastest way to get to the root of the problem.

Interacting with Active Record objects

At the end of the day, Active Record objects are just normal Ruby objects. As such, we can define methods on them which perform arbitrary actions.

For example, GitLab developers have added some methods which help with two-factor authentication: