Rails 콘솔

Tier: Free, Premium, Ultimate Offering: Self-managed

GitLab의 핵심은 Ruby on Rails 프레임워크를 사용하여 구축된 웹 애플리케이션에 있습니다. Rails 콘솔은 GitLab 인스턴스와 명령 줄에서 상호 작용하고 Rails에 내장된 놀라운 도구에 액세스할 수 있는 방법을 제공합니다.

경고: Rails 콘솔은 GitLab과 직접 상호 작용합니다. 많은 경우, 생산 데이터를 영구적으로 수정, 손상 또는 파괴하지 않도록 방지할 수 있는 보호 막이 없습니다. 어떠한 결과도 없이 Rails 콘솔을 탐색하려면 테스트 환경에서 권장합니다.

Rails 콘솔은 문제를 해결하거나 GitLab 애플리케이션에 직접 액세스해야 하는 GitLab 시스템 관리자를 위한 것입니다. Ruby의 기본 지식이 필요합니다(빠른 소개를 위해 이 30분 튜토리얼을 시도해보세요). Rails 경험이 유용하지만 필수는 아닙니다.

Rails 콘솔 세션 시작하기

Rails 콘솔 세션을 시작하는 프로세스는 GitLab 설치 유형에 따라 다릅니다.

리눅스 패키지 (Omnibus)
sudo gitlab-rails console
도커
docker exec -it <container-id> gitlab-rails console
직접 컴파일 (소스)
sudo -u git -H bundle exec rails console -e production
Helm 차트 (쿠버네티스)
# 팟을 찾기
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

:::Warn REPLACED BY RL098

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

메소드 소스 찾기

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)

마지막 두 명령이 여러 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 문을 연쇄시킬 수 있어보이는 것을 주목하셨나요? 또한 반환된 컬렉션이 단일 객체만을 포함하는 경우에도 직접적으로 상호작용할 수 없다는 것에 주목할 것입니다:

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 문서를 참조하십시오. ```

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: 'hunter2@example.com')

이제 다른 방법을 시도해 봅시다:

# 최신 상태의 객체를 다시 검색합니다
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에서 !로 끝나는 메서드 이름은 보통 “뱅 메서드”로 알려져 있습니다. 관례상 뱅은 메서드가 변환된 결과를 반환하지 않고 기존 객체를 직접 수정한다는 것을 나타내며, 데이터베이스에 쓰기 작업을 수행하는 Active Record 메서드의 경우 뱅 메서드는 오류가 발생할 때마다 명시적 예외를 발생시켜 단순히 false를 반환하는 대신 예외를 발생시킵니다.

또한 유효성 검사를 완전히 건너뛸 수도 있습니다:

# 최신 상태의 객체를 다시 검색하여 최신 상태를 얻습니다
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save!(validate: false)

이것은 권장되지는 않으며, 보통 유효성 검사는 사용자가 제공한 데이터의 무결성과 일관성을 보장하기 위해 적용됩니다.

유효성 오류는 전체 객체의 데이터베이스에 저장을 방지합니다. 이것에 대한 약간의 예시를 아래 섹션에서 볼 수 있습니다. GitLab UI에서 어떤 식으로든 폼을 제출할 때 신비한 빨간 배너가 나타난다면, 문제의 근본원인을 파악하는 가장 빠른 방법입니다.

Active Record 객체와 상호 작용

마지막으로 Active Record 객체는 일반적인 루비 객체일 뿐입니다. 따라서 임의 작업을 수행하는 메서드를 정의할 수 있습니다.

예를 들어, 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 또는 루비 소프트웨어 패키지에 의해 정의됩니다. 예를 들어, 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 관리 설정에서 때때로 유효성 검사가 추가되거나 변경되어 이전에 저장된 설정이 이제 유효성 검사에 실패하는 경우가 종종 있습니다. 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)을 수행하는 scope 편의 메서드입니다.

경로로 프로젝트 가져오기:

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”를 의미하며, 각 GitLab 프로젝트에 대해 이슈 및 병합 요청 ID를 범위로 지정하는 방법입니다.

경로로 그룹 가져오기:

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’에서 객체 열기

경고: 데이터를 변경하는 명령은 올바른 조건에서 또는 정확히 실행되지 않을 경우에는 손상을 일으킬 수 있습니다. 항상 명령을 테스트 환경에서 먼저 실행하고 복원할 백업 인스턴스가 준비되어 있는지 확인하세요.

가끔은 객체의 컨텍스트에서 메서드를 통과하는 것이 더 쉬울 수 있습니다. Object의 네임스페이스에 observ를 열어 객체의 컨텍스트에서 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 syntax error

gitlab-rails 명령은 기본적으로 git:git 그룹을 사용하는 비루트 계정과 그룹을 사용하여 Rails Runner를 실행합니다.

루비 스크립트 파일 이름이 gitlab-rails runner에 전달되는 명령에서 비루트 계정이 파일을 찾을 수 없다면 구문 오류가 아니라 파일에 액세스할 수 없는 오류가 발생할 수 있습니다.

이러한 이유로 스크립트가 루트 계정의 홈 디렉토리에 위치한 경우가 있습니다.

runner는 경로와 파일 매개변수를 루비 코드로 구문 분석하려고 시도합니다.

예를 들어:

[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