기업용 버전 기능 구현 지침

  • ee/에 코드 배치: 기업용 버전(EE) 모든 코드를 ee/ 최상위 디렉토리에 배치하세요. 나머지 코드는 가능한 CE(Community Edition) 파일에 가깝게 위치해야 합니다.
  • 테스트 작성: 모든 코드와 마찬가지로, EE 기능은 회귀를 방지하기 위해 좋은 테스트 커버리지를 가져야 합니다. 모든 ee/ 코드는 ee/에 해당하는 테스트를 가져야 합니다.
  • 문서 작성: doc/ 디렉토리에 문서를 추가하세요. 해당 기능을 설명하고, 적용 가능한 경우 스크린샷을 첨부하세요. 기능이 어떤 버전에 적용되는지를 나타내세요 버전

SaaS 전용 기능

SaaS 전용(예: CustomersDot 통합)으로 적용되는 기능을 개발할 때 다음 지침을 사용하세요.

일반적으로 기능은 SaaS 및 Self-Managed 배포 모두에 제공되어야 합니다. 그러나 경우에 따라 특정 기능을 SaaS에서만 사용할 수 있도록 설정해야 하는 경우가 있습니다. 이 가이드에서는 해당 방법을 설명합니다.

Gitlab::Saas.feature_available?를 사용하는 것이 권장됩니다. 이를 통해 해당 기능이 SaaS 전용임을 상세하게 지정할 수 있습니다.

Gitlab::Saas.feature_available?를 사용하여 SaaS 전용 기능 구현

FEATURES 상수에 추가

  1. 새 SaaS 전용 기능의 이름을 정하는 데 도움이 되도록 네임스페이싱 컨셉 가이드를 참조하세요.
  2. ee/lib/ee/gitlab/saas.rbFEATURE에 새 기능을 추가하세요.

    FEATURES = %i[purchases_additional_minutes some_domain_new_feature_name].freeze
    
  3. 코드에서 Gitlab::Saas.feature_available?(:some_domain_new_feature_name)로 새 기능을 사용하세요.

SaaS 전용 기능 정의 및 검증

이 프로세스는 코드베이스 내에서 일관된 SaaS 기능 사용을 보장하기 위한 것입니다. 모든 SaaS 기능은 반드시 다음을 준수해야 합니다:

  • 알려진 상태여야 합니다. 명시적으로 정의된 SaaS 기능만 사용하세요.
  • 소유자가 있어야 합니다.

모든 SaaS 기능은 다음 경로에 저장된 YAML 파일에 자체 문서화되어 있습니다:

각 SaaS 기능은 여러 필드로 구성된 별도의 YAML 파일에 정의됩니다:

필드 필수 여부 설명
name SaaS 기능의 이름
introduced_by_url 아니오 SaaS 기능을 도입한 병합 요청의 URL
milestone 아니오 SaaS 기능이 생성된 마일스톤
group 아니오 기능 플래그를 소유하는 그룹

새 SaaS 기능 파일 정의 작성

GitLab 코드베이스는 새 SaaS 기능 정의를 생성하는 전용 도구인 bin/saas-feature.rb를 제공합니다. 이 도구는 새 SaaS 기능에 대해 여러 질문을 하고, 그런 다음 ee/config/saas_features에 YAML 정의를 작성합니다.

개발 또는 테스트 환경을 실행할 때 YAML 정의 파일이 있는 SaaS 기능만 사용할 수 있습니다.

❯ bin/saas-feature my_saas_feature
'group::acquisition' 그룹을 선택하셨습니다.

>> SaaS 기능을 소개한 MR(병합 요청)의 URL(비워두고 Danger가 MR에서 직접 제안하도록 허용):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
create ee/config/saas_features/my_saas_feature.yml
---
name: my_saas_feature
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
milestone: '16.8'
group: group::acquisition

다른 SaaS 인스턴스(JiHu)에서 SaaS 전용 기능 비활성화

ee/lib/ee/gitlab/saas.rb 모듈을 덮어쓰고 Gitlab::Saas.feature_available? 메서드를 재정의하세요.

JH_DISABLED_FEATURES = %i[some_domain_new_feature_name].freeze

override :feature_available?
def feature_available?(feature)
  super && JH_DISABLED_FEATURES.exclude?(feature)
end

CE 기능에 대한 SaaS 전용 기능 사용 금지

Gitlab::Saas.feature_available?는 CE에서 사용하면 안 됩니다. EE 백엔드 코드로 CE 확장 가이드를 참조하세요.

테스트에서 SaaS 전용 기능

코드베이스에 새 SaaS 전용 기능을 도입하면 테스트해야 할 추가 코드 경로가 생성됩니다. SaaS 전용 기능과 관련된 모든 코드에 대해 테스트를 수행하는 것이 강력히 권장됩니다. 기능이 활성화될 때와 비활성화될 때 모두 자동화된 테스트를 포함하는 것이 좋습니다.

테스트에서 SaaS 전용 기능을 활성화하려면 stub_saas_features 헬퍼를 사용하세요. 예를 들어, 테스트에서 ‘purchases_additional_minutes’ 기능 플래그를 글로벌로 비활성화하려면:

stub_saas_features(purchases_additional_minutes: false)

::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false

양쪽 경로에 대해 테스트하는 일반적인 패턴은 다음과 같습니다:

it 'purchases/additional_minutes를 사용할 수 없습니다' do
  # purchases_additional_minutes가 기본적으로 비활성화되었다고 가정한 테스트
  ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
end

context 'purchases_additional_minutes를 사용할 수 있는 경우' do
  before do
    stub_saas_features(purchases_additional_minutes: true)
  end

  it '참을 반환합니다' do
    ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
  end
end

SaaS 인스턴스 시뮬레이션

로컬에서 개발 중이고 제품의 SaaS(GitLab.com) 버전을 시뮬레이트해야하는 경우 다음 단계를 따르세요:

  1. 이 환경 변수를 내보냅니다.

    export GITLAB_SIMULATE_SAAS=1
    

    로컬 GitLab 인스턴스에 환경 변수를 전달하는 여러 가지 방법이 있습니다.
    예를 들어, 위 스니펫을 사용하여 GDK의 루트에 env.runit 파일을 생성할 수 있습니다.

  2. 라이선스된 EE 기능 사용 허용을 활성화하여 프로젝트의 라이선스된 EE 기능을 해당 프로젝트 네임스페이스의 계획에서만 사용할 수 있도록 합니다.

    1. 왼쪽 사이드바에서 밑으로 스크롤하여 관리자를 선택합니다.
    2. 왼쪽 사이드바에서 설정 > 일반을 선택합니다.
    3. 계정 및 제한을 펼칩니다.
    4. 라이선스된 EE 기능 사용 허용 확인란을 선택합니다.
    5. 변경 사항 저장을 선택합니다.
  3. 테스트할 그룹이 실제로 EE 계획을 사용하고 있는지 확인하세요:

    1. 왼쪽 사이드바에서 밑으로 스크롤하여 관리자를 선택합니다.
    2. 왼쪽 사이드바에서 개요 > 그룹을 선택합니다.
    3. 수정하고자 하는 그룹을 식별하고 편집을 선택합니다.
    4. 권한 및 그룹 기능으로 스크롤하여 계획에서 Ultimate을 선택합니다.
    5. 변경 사항 저장을 선택합니다.

새로운 EE 기능 구현

GitLab Premium 또는 GitLab Ultimate 라이선스 기능을 개발 중이라면, 다음 단계를 사용하여 새로운 기능을 추가하거나 확장하세요.

GitLab 라이선스 기능은 ee/app/models/gitlab_subscriptions/features.rb에 추가됩니다.
이 파일을 어떻게 수정할 지 결정하려면 먼저 제품 매니저와 라이선싱 절차에 대해 논의하세요.

다음 질문들을 사용하여 진행하세요:

  1. 이것은 새로운 기능인가요, 아니면 기존 라이선스 기능을 확장하는 건가요?
    • 기능이 이미 존재한다면 features.rb를 수정할 필요는 없지만, 기존 기능 식별자를 찾아 보호해야 합니다.
    • 이것이 새로운 기능이라면 features.rb 파일에 추가할 식별자(예: my_feature_name)를 결정하세요.
  2. 이것은 GitLab Premium 또는 GitLab Ultimate 기능인가요?
    • 사용할 기능에 대한 계획에 기반하여 PREMIUM_FEATURES 또는 ULTIMATE_FEATURES에 기능 식별자를 추가하세요.
  3. 이 기능은 전역적으로(시스템 전체적으로 GitLab 인스턴스에서) 사용 가능한가요?
    • Geo데이터베이스 로드 밸런싱과 같은 기능은 인스턴스 라이선스에서 정의되는 전체 인스턴스에서 사용되며 개별 사용자 네임스페이스로 제한할 수 없습니다. 이러한 기능을 GLOBAL_FEATURES에 추가하세요.

EE 기능 보호

라이선스 기능은 라이선스가 있는 사용자에게만 사용할 수 있습니다. 기능에 액세스할 수 있는지 확인하기 위해 확인 또는 보호를 추가해야 합니다.

라이선스 기능을 보호하려면:

  1. ee/app/models/gitlab_subscriptions/features.rb에서 기능 식별자를 찾습니다.
  2. 다음과 같은 방법을 사용하여 my_feature_name이 기능 식별자인 경우:

    • 프로젝트 콘텍스트에서:

      my_project.licensed_feature_available?(:my_feature_name) # my_project에서 사용 가능한 경우 true
      
    • 그룹 또는 사용자 네임스페이스 콘텍스트에서:

      my_group.licensed_feature_available?(:my_feature_name) # my_group에서 사용 가능한 경우 true
      
    • 전역(시스템 전체) 기능인 경우:

      License.feature_available?(:my_feature_name)  # 이 인스턴스에서 사용 가능한 경우 true
      
  3. 선택 사항입니다. 전역 기능이 유료 계획을 가진 네임스페이스에서도 사용 가능한 경우 두 기능 식별자를 결합하여 관리자 및 그룹 사용자 둘 다 사용할 수 있도록 할 수 있습니다. 예를 들어:

    License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # 관리자 및 그룹 멤버 모두 이 EE 기능을 볼 수 있음
    

라이선스가 없을 때 CE 인스턴스 시뮬레이션

GitLab CE 기능을 라이선스가 없는 EE 인스턴스와 작업하도록 구현한 후,
GitLab 엔터프라이즈 에디션은 라이선스가 활성화되지 않은 경우 GitLab 커뮤니티 에디션과 같이 작동합니다.

CE 사양은 최대한 건드리지 않고 EE를 위해 추가 사양을 더할 수 있습니다.
라이선스 기능은 EE::LicenseHelpersstub_licensed_features를 사용하여 스텁 처리될 수 있습니다.

GitLab을 CE로 강제로 동작하려면 ee/ 디렉토리를 삭제하거나
FOSS_ONLY 환경 변수true로 계산되는 값으로 설정하면 됩니다.
테스트 실행 시에도 동일하게 적용됩니다 (예: FOSS_ONLY=1 yarn jest).

라이선스가 있는 GDK로 CE 인스턴스 시뮬레이션

GDK에서 라이선스를 삭제하지 않고 CE 인스턴스를 모의할 수 있습니다:

  1. GDK의 루트에 env.runit 파일을 생성하고 다음 줄을 추가합니다:

    export FOSS_ONLY=1
    
  2. 그런 다음 GDK를 다시 시작합니다:

    gdk restart rails && gdk restart webpack
    

다시 EE 설치로 되돌아가려면 env.runit에서 줄을 지우고 단계 2를 반복하세요.

CE로 기능 사양 실행

기능 사양을 CE로 실행하는 경우 백엔드 및 프론트엔드 버전이 일치하는지 확인해야 합니다.
다음을 수행하세요:

  1. FOSS_ONLY=1 환경 변수를 설정합니다:

    export FOSS_ONLY=1
    
  2. GDK를 시작합니다:

    gdk start
    
  3. 기능 사양을 실행합니다:

    bin/rspec spec/features/<경로_도도파일>
    

FOSS 콘텍스트에서 CI 파이프라인 실행

기본적으로 개발을 위한 머지 리퀘스트 파이프라인은 EE 콘텍스트에서만 실행됩니다.
FOSS 및 EE 간에 다른 기능을 개발 중이라면 FOSS 콘텍스트에서도 파이프라인을 실행하고 싶어할 수 있습니다.

두 콘텍스트에서 파이프라인을 실행하려면 머지 리퀘스트에 ~"pipeline:run-as-if-foss" 레이블을 추가하세요.

자세한 내용은 FOSS처럼 실행 및 프로젝트 간 하향 파이프라인 파이프라인 문서를 참조하세요.

백엔드에서 EE 코드 분리

EE 전용 기능

개발 중인 기능이 CE에 어떤 형태로든 존재하지 않는 경우, 코드를 EE 네임스페이스에 넣을 필요가 없습니다. 예를 들어, EE 모델은 Awesome를 클래스 이름으로 사용하여 ee/app/models/awesome.rb에 들어갈 수 있습니다. 이는 모델에만 적용되는 것이 아닙니다. 다음은 다른 예시 목록입니다:

  • ee/app/controllers/foos_controller.rb
  • ee/app/finders/foos_finder.rb
  • ee/app/helpers/foos_helper.rb
  • ee/app/mailers/foos_mailer.rb
  • ee/app/models/foo.rb
  • ee/app/policies/foo_policy.rb
  • ee/app/serializers/foo_entity.rb
  • ee/app/serializers/foo_serializer.rb
  • ee/app/services/foo/create_service.rb
  • ee/app/validators/foo_attr_validator.rb
  • ee/app/workers/foo_worker.rb
  • ee/app/views/foo.html.haml
  • ee/app/views/foo/_bar.html.haml

이 작업은 CE eager-load/auto-load 경로에 대해 모든 경로에 대해 동일한 ee/로 시작하는 경로를 config/application.rb에 추가합니다. 이는 뷰에도 적용됩니다.

EE 전용 백엔드 기능 테스트

CE에 존재하지 않는 EE 클래스를 테스트하려면 보통대로 ee/spec 디렉토리에 특정 ee/ 하위 디렉토리 없이 특정 ee/ 디렉토리에 특정 ee/ 하위 디렉토리 없이 특정 ee/ 디렉토리에 테스트 파일을 만듭니다. 예를 들어, ee/app/models/vulnerability.rb 클래스를 위한 테스트 파일은 ee/spec/models/vulnerability_spec.rb에 위치합니다.

기본적으로 라이선스가 적용된 기능은 스펙에 대해 비활성화되어 있습니다. ee/spec 디렉토리에 있는 스펙은 기본적으로 Starter 라이선스가 초기화됩니다.

기능을 효과적으로 테스트하려면 stub_licensed_features 도우미를 사용하여 기능을 명시적으로 활성화해야 합니다. 예를 들어:

  stub_licensed_features(my_awesome_feature_name: true)

CE 기능을 EE 백엔드 코드로 확장

기존 CE 기능을 기반으로 하는 기능에 대해, EE 네임스페이스에 모듈을 작성하고 CE 클래스에 삽입하세요. 클래스가 속한 파일의 마지막 행에 모듈을 삽입하면 CE에서 EE로 병합될 때 충돌이 발생할 가능성이 줄어듭니다. 예를 들어, User 클래스에 모듈을 삽입하려면 다음 접근 방식을 사용합니다:

class User < ActiveRecord::Base
  # ... 여기에 많은 코드가 있습니다 ...
end

User.prepend_mod

prepend, extend, include와 같은 방법을 사용하지 마세요. 대신, prepend_mod, extend_mod, 또는 include_mod을 사용하세요. 이러한 방법은 수신 모듈의 이름으로 관련된 EE 모듈을 찾으려고 시도합니다. 예를 들어;

module Vulnerabilities
  class Finding
    #...
  end
end

Vulnerabilities::Finding.prepend_mod

::EE::Vulnerabilities::Finding이라는 모듈을 앞에 놓습니다.

확장 모듈이 이 네이밍 규칙을 따르지 않는 경우, prepend_mod_with, extend_mod_with, 또는 include_mod_with를 사용하여 모듈 이름을 제공할 수 있습니다. 이러한 방법은 해당 모듈 자체가 아닌 전체 모듈 이름을 포함하는 _String_을 인수로 취합니다. 예를 들어;

class User
  #...
end

User.prepend_mod_with('UserExtension')

확장 모듈에 EE 네임스페이스가 필요하므로 파일은 ee/ 하위 디렉토리에 위치해야 합니다. 예를 들어, 우리가 EE에서 사용자 모델을 확장하려고 하면, ::EE::User라는 모듈을 ee/app/models/ee/user.rb에 넣습니다.

이는 모델에만 적용되는 것이 아닙니다. 다음은 다른 예시 목록입니다:

  • ee/app/controllers/ee/foos_controller.rb
  • ee/app/finders/ee/foos_finder.rb
  • ee/app/helpers/ee/foos_helper.rb
  • ee/app/mailers/ee/foos_mailer.rb
  • ee/app/models/ee/foo.rb
  • ee/app/policies/ee/foo_policy.rb
  • ee/app/serializers/ee/foo_entity.rb
  • ee/app/serializers/ee/foo_serializer.rb
  • ee/app/services/ee/foo/create_service.rb
  • ee/app/validators/ee/foo_attr_validator.rb
  • ee/app/workers/ee/foo_worker.rb

CE 기능을 기반으로 하는 EE 기능 테스트

CE 클래스를 EE 기능으로 확장하는 EE 네임스페이스 모듈을 테스트하려면 ee/spec 디렉토리에 일반적으로 ee/ 하위 디렉토리를 포함하여 특정 ee/ 디렉토리에 테스트 파일을 만듭니다. 예를 들어, ee/app/models/ee/user.rb를 작성하는 경우 해당 테스트는 ee/spec/models/ee/user_spec.rb에 작성됩니다.

RSpec.describe 호출에서, EE 모듈이 사용된 CE 클래스 이름을 사용합니다. 예를 들어, ee/spec/models/ee/user_spec.rb에서 테스트는 다음과 같이 시작합니다.

RSpec.describe User do
  describe 'ee feature added through extension'
end

CE 메소드 재정의

CE 코드베이스에 있는 메소드를 재정의하려면 prepend를 사용하세요. 이를 통해 클래스의 메소드를 모듈의 메소드로 재정의할 수 있으면서도 super를 사용하여 클래스의 구현에 액세스할 수 있습니다.

이에 관련해서 고려해야 할 사항이 있습니다:

  • 항상 extend ::Gitlab::Utils::Override를 사용하여 override를 보호해야 합니다. CE에서 메소드 이름이 변경되더라도 EE 재정의가 누락되지 않도록 보장합니다.
  • overrider 메소드가 CE 구현의 중간에 행을 추가할 때는 CE 메소드를 리팩터링하고 더 작은 메소드로 나누거나, CE에 빈 “hook” 메소드를 만들어 EE에 특화된 구현을 만들어야 합니다.
  • 기존 구현에 가드 절(예: return unless condition)이 포함되어 있는 경우, 메소드를 재정의하여 동작을 확장하는 것이 쉽지 않습니다. 왜냐하면 재정의된 메소드(즉, 재정의된 메소드에서 super를 호출하는 것)가 언제 일찌감치 멈추어야 하는지를 알 수 없기 때문입니다. 이 경우 우리가 단순히 재정의하는 것이 아니라 기본 메소드를 업데이트하여 원하는 다른 메소드를 호출하도록 해야 합니다. 예를 들어, 이러한 기본이 주어진 경우:

      class Base
        def execute
          return unless enabled?
    
          # ...
          # ...
        end
      end
    

    Base#execute를 단순히 재정의 대신, 메소드를 업데이트하고 다른 메소드로 동작을 추출합니다:

      class Base
        def execute
          return unless enabled?
    
          do_something
        end
    
        private
    
        def do_something
          # ...
          # ...
        end
      end
    

    그런 다음 가드가 걱정없이 do_something을 재정의할 수 있습니다:

      module EE::Base
        extend ::Gitlab::Utils::Override
    
        override :do_something
        def do_something
          # 상기 패턴을 따라 super를 호출하고 확장합니다
        end
      end
    

prepend하는 경우 ee/ 특정 하위 디렉토리에 배치하고 충돌하는 이름을 피하기 위해 클래스 또는 모듈을 module EE로 감싸세요.

예를 들어, ApplicationController#after_sign_out_path_for에서 CE 구현을 재정의하는 경우:

def after_sign_out_path_for(resource)
  current_application_settings.after_sign_out_path.presence || new_user_session_path
end

메소드를 그대로 수정하는 대신 기존 파일에 prepend를 추가하세요:

class ApplicationController < ActionController::Base
  # ...

  def after_sign_out_path_for(resource)
    current_application_settings.after_sign_out_path.presence || new_user_session_path
  end

  # ...
end

ApplicationController.prepend_mod_with('ApplicationController')

그리고 변경된 구현을 가진 ee/ 하위 디렉토리에 새 파일을 만드세요:

module EE
  module ApplicationController
    extend ::Gitlab::Utils::Override

    override :after_sign_out_path_for
    def after_sign_out_path_for(resource)
      if Gitlab::Geo.secondary?
        Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
      else
        super
      end
    end
  end
end
CE 클래스 메소드 재정의

클래스 메소드에도 마찬가지로 ActiveSupport::Concern을 사용하고 class_methods 블록 내에 extend ::Gitlab::Utils::Override를 넣으려 합니다. 다음은 예시입니다:

module EE
  module Groups
    module GroupMembersController
      extend ActiveSupport::Concern

      class_methods do
        extend ::Gitlab::Utils::Override

        override :admin_not_required_endpoints
        def admin_not_required_endpoints
          super.concat(%i[update override])
        end
      end
    end
  end
end

자체 설명 래퍼 메소드 사용

메소드의 구현을 수정하는 것이 불가능하거나 논리적이지 않을 때, 해당 메소드를 자체 설명 메소드로 래핑하고 그 메소드를 사용합니다.

예를 들어, GitLab-FOSS에서 시스템에 의해 생성된 유일한 사용자는 Users::Internal.ghost입니다. 그러나 EE에는 실제로 사용자가 아닌 여러 유형의 봇 사용자가 있습니다. User#ghost?의 구현을 재정의하는 것은 부적절하기 때문에 app/models/user.rb#internal? 메소드를 추가합니다. 구현은 다음과 같습니다:

def internal?
  ghost?
end

EE에서 ee/app/models/ee/users.rb의 구현은 다음과 같을 것입니다:

override :internal?
def internal?
  super || bot?
end

config/routes에 코드 추가

config/routes.rbdraw :admin을 추가하면 애플리케이션은 config/routes/admin.rb에 있는 파일을 로드하려고 시도합니다. 또한 ee/config/routes/admin.rb에 있는 파일도 로드하려고 시도합니다.

EE에서는 최소 한 파일, 최대 두 파일을 로드해야 합니다. 어떤 파일도 찾을 수 없으면 오류가 발생합니다. CE에서는 EE 경로가 있는지 여부를 알 수 없으므로 아무것도 찾을 수 없더라도 오류가 발생하지 않습니다.

따라서 특정 CE 경로 파일을 확장하려면 ee/config/routes에 동일한 파일을 추가하면 되고, EE 전용 경로를 추가하려면 CE 및 EE에 모두 draw :ee_only를 넣고 ee/config/routes/ee_only.rb을 추가하면 됩니다. 이는 render_if_exists와 유사합니다.

app/controllers/의 코드

컨트롤러에서 가장 일반적인 충돌 유형은 CE에서는 목록에 있는 액션을 가진 before_action인 반면, EE에서는 해당 목록에 일부 액션을 추가하는 것입니다.

동일한 문제가 종종 params.require/params.permit 호출에 대해 발생합니다.

줄이는 방법

CE 및 EE 액션/키워드를 분리하세요. 예를 들어 ProjectsControllerparams.require 경우 다음과 같습니다:

def project_params
  params.require(:project).permit(project_params_attributes)
end

# 사용 사례에 가장 적합한 방법으로 생성된 기호 배열을 항상 반환합니다.
# 알파벳순으로 정렬되어야 합니다.
def project_params_attributes
  %i[
    description
    name
    path
  ]
end

EE::ProjectsController 모듈에서:

def project_params_attributes
  super + project_params_attributes_ee
end

def project_params_attributes_ee
  %i[
    approvals_before_merge
    approver_group_ids
    approver_ids
    ...
  ]
end

app/models/의 코드

EE 전용 모델은 ee/app/models/에 정의되어야 합니다.

CE 모델을 재정의하려면 ee/app/models/ee/에 파일을 만들고 prepended 블록에 새 코드를 추가하면 됩니다.

ActiveRecord enums는 완전히 FOSS에 정의되어야 합니다.

app/views/의 코드

EE가 CE 뷰에 일부 특정 뷰 코드를 추가하는 것은 매우 빈번한 문제입니다. 예를 들어, 프로젝트 설정 페이지의 승인 코드입니다.

줄이는 방법

EE 전용 코드 블록은 part로 이동해야 합니다. 이렇게 하면 들여쓰기가 추가되어 충돌이 발생하는 HAML 코드 뭉치와 충돌을 피할 수 있습니다.

EE 전용 뷰는 ee/app/views/에 있어야 하며, 적절한 경우 추가 하위 디렉토리를 사용해야 합니다.

render_if_exists 사용

일반 render대신 render_if_exists를 사용해야 합니다. render_if_exists는 특정 파셜을 찾을 수 없는 경우 아무것도 렌더링하지 않습니다. 이를 통해 CE와 EE 사이의 코드를 동일하게 유지할 수 있습니다.

이의 장점:

  • CE 코드를 읽을 때 EE 뷰를 확장하는 위치에 대한 명확한 힌트가 됩니다.

이의 단점:

  • 부분 이름에 오타가 있을 경우 무시될 수 있습니다.
주의 사항

render_if_exists 뷰 경로 인수는 app/views/ee/app/views/를 기준으로 상대적이어야 합니다. CE 뷰 경로를 기준으로 한 EE 템플릿 경로를 해결하는 것이 동작하지 않습니다.

- # app/views/projects/index.html.haml

= render_if_exists 'button' # `ee/app/views/projects/_button`를 렌더링하지 않고 조용히 실패합니다
= render_if_exists 'projects/button' # `ee/app/views/projects/_button`을 렌더링합니다

render_ce 사용

renderrender_if_exists는 먼저 EE 파셜을 찾은 후 CE 파셜을 찾습니다. 특정 파셜만 렌더링되며, 반복 호출 되지 않습니다. 동일한 파셜 경로가 CE에는 CE 파셜 (app/views/projects/settings/_archive.html.haml)이고 EE에서는 EE 파셜 (ee/app/views/projects/settings/_archive.html.haml)을 참조하도록 가능합니다. 이렇게하면 CE와 EE간에 다른 내용을 표시할 수 있습니다.

그러나 때로는 기존 CE 파셜에 추가할 내용이 필요한 경우도 있습니다. 이 문제는 다른 이름을 가진 다른 파셜을 추가하여 해결할 수 있지만, 번거로울 수 있습니다.

이 경우 render_ce를 사용하여 EE 파셜을 무시하도록 할 수 있습니다. 이는 ee/의 부분을 무시하고 CE 파셜(app/views/projects/settings/_archive.html.haml)을 렌더링하게 해줍니다.

lib/gitlab/background_migration/의 코드

EE 전용 백그라운드 마이그레이션을 작성할 때, GitLab EE를 CE로 다운그레이드하는 사용자를 고려해야 합니다. 다시 말해서, 모든 EE 전용 마이그레이션은 CE 코드에 있어야 하지만 구현은 없어야 합니다. 대신 EE 측에서 확장해야 합니다.

GitLab CE:

# lib/gitlab/background_migration/prune_orphaned_geo_events.rb

module Gitlab
  module BackgroundMigration
    class PruneOrphanedGeoEvents
      def perform(table_name)
      end
    end
  end
end

Gitlab::BackgroundMigration::PruneOrphanedGeoEvents.prepend_mod_with('Gitlab::BackgroundMigration::PruneOrphanedGeoEvents')

GitLab EE:

# ee/lib/ee/gitlab/background_migration/prune_orphaned_geo_events.rb

module EE
  module Gitlab
    module BackgroundMigration
      module PruneOrphanedGeoEvents
        extend ::Gitlab::Utils::Override

        override :perform
        def perform(table_name = EVENT_TABLES.first)
          return if ::Gitlab::Database.read_only?

          deleted_rows = prune_orphaned_rows(table_name)
          table_name   = next_table(table_name) if deleted_rows.zero?

          ::BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, self.class.name.demodulize, table_name) if table_name
        end
      end
    end
  end
end

app/graphql/의 코드

EE 전용 뮤테이션, 리졸버 및 타입은 ee/app/graphql/{mutations,resolvers,types}에 추가되어야 합니다.

CE 뮤테이션, 리졸버 또는 타입을 재정의하려면 파일을 만들고 prepended 블록에 새 코드를 추가합니다.

예를 들어, CE에 Mutations::Tanukis::Create라는 뮤테이션이 있고 새로운 매개변수를 추가하려면, EE 재정의 파일을 다음 위치에 만듭니다.

module EE
  module Mutations
    module Tanukis
      module Create
        extend ActiveSupport::Concern

        prepended do
          argument :name,
                   GraphQL::Types::String,
                   required: false,
                   description: 'Tanuki name'
        end
      end
    end
  end
end

lib/의 코드

EE 특정 로직은 최상위 EE 모듈 네임스페이스에 배치합니다. 클래스의 네임스페이스는 보통대로 EE 모듈 아래에 있어야 합니다.

예를 들어, CE에 lib/gitlab/ldap/에 LDAP 클래스가 있는 경우, EE 특정 LDAP 클래스는 ee/lib/ee/gitlab/ldap에 배치해야 합니다.

lib/api/의 코드

EE 기능을 단일 prepend_mod_with 라인으로 확장하는 것은 매우 까다로울 수 있습니다. 각 다른 Grape 기능마다 다른 전략이 필요할 수 있습니다. 다양한 전략을 쉽게 적용하기 위해, 우리는 EE 모듈에서 extend ActiveSupport::Concern을 사용할 것입니다.

EE 백엔드 코드로 CE 기능 확장를 따라 EE 모듈 파일을 배치합니다.

EE API 라우트

EE API 라우트에서는 prepended 블록에 배치합니다.

module EE
  module API
    module MergeRequests
      extend ActiveSupport::Concern

      prepended do
        params do
          requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
        end
        resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
          # ...
        end
      end
    end
  end
end

일부 상수에 대한 전체 한정자를 사용해야 합니다.

EE 파라미터

파라미터를 정의하고 CE에서 오버라이드할 수 있도록 “인터페이스”를 먼저 정의해야 합니다. Grape는 내부적으로 복잡하기 때문에 인터페이스를 먼저 정의하고 후에 오버라이드하기 위해 필요합니다.

예를 들어, EE에 몇 가지 추가 선택적 매개변수가 있는 경우 CE API params는 다음과 같습니다.

module API
  module Helpers
    module ProjectsHelpers
      extend ActiveSupport::Concern
      extend Grape::API::Helpers

      params :optional_project_params_ce do
        # CE 특정 매개변수가 여기에 있습니다...
      end

      params :optional_project_params_ee do
      end

      params :optional_project_params do
        use :optional_project_params_ce
        use :optional_project_params_ee
      end
    end
  end
end

API::Helpers::ProjectsHelpers.prepend_mod_with('API::Helpers::ProjectsHelpers')

우리는 EE 모듈에서 이를 오버라이드할 수 있습니다.

EE 헬퍼

EE 모듈이 CE 헬퍼를 쉽게 오버라이드하려면 먼저 확장하려는 헬퍼를 정의해야 합니다. 그런 다음, 그것을 오버라이드할 수 있도록 일반적인 객체지향 관행을 따를 것입니다.

EE 특정 동작

때로는 일부 API에서 EE 특정 동작이 필요할 수 있습니다. 보통은 CE 메소드를 EE 메소드로 오버라이드할 수 있겠지만, API 라우트는 메소드가 아니므로 오버라이드할 수 없습니다. 독립된 메소드로 추출하거나 CE 라우트에 동작을 삽입할 수 있는 “훅”을 도입해야 합니다.

EE route_setting

EE 모듈에서는 이를 확장하는 것이 매우 어려우며, 이것은 특정 라우트에 대한 메타데이터를 저장하고 있습니다. 그렇기 때문에 CE에 있는 EE route_setting을 남겨둘 수 있으며 CE에서는 해당 메타데이터를 사용하지 않기 때문에 문제가 되지 않습니다.

우리는 route_setting을 더 자주 사용하게 되고 확장해야 하는지 여부를 고려할 수 있습니다. 지금은 그것을 그다지 사용하지 않고 있습니다.

Utilizing class methods for setting up EE-specific data

가끔씩 특정 API 라우트에 대해 다른 인자를 사용해야 할 때가 있습니다. 그리고 Grape는 다른 컨텍스트에서 다르게 작용하기 때문에 이를 쉽게 EE 모듈로 확장할 수 없습니다. 따라서 이를 극복하기 위해 해당 데이터를 별도의 모듈이나 클래스에 있는 클래스 메서드로 이동해야 합니다. 이를 통해 데이터를 사용하기 전에 해당 모듈 또는 클래스를 확장할 수 있게 되며, CE 코드 중간에 prepend_mod_with를 놓을 필요가 없게 됩니다.

예를 들어, 어떤 곳에서는 at_least_one_of에 추가적인 인자를 전달하여 API가 EE 전용 인자를 가장 작은 인자로 고려하도록 해야 할 수 있습니다. 우리는 다음과 같이 접근할 수 있습니다:

# api/merge_requests/parameters.rb
module API
  class MergeRequests < Grape::API::Instance
    module Parameters
      def self.update_params_at_least_one_of
        %i[
          assignee_id
          description
        ]
      end
    end
  end
end

API::MergeRequests::Parameters.prepend_mod_with('API::MergeRequests::Parameters')

# api/merge_requests.rb
module API
  class MergeRequests < Grape::API::Instance
    params do
      at_least_one_of(*Parameters.update_params_at_least_one_of)
    end
  end
end

그리고 나서 우리는 쉽게 그 EE 클래스 메서드에서 해당 인자를 확장할 수 있습니다:

module EE
  module API
    module MergeRequests
      module Parameters
        extend ActiveSupport::Concern

        class_methods do
          extend ::Gitlab::Utils::Override

          override :update_params_at_least_one_of
          def update_params_at_least_one_of
            super.push(*%i[
              squash
            ])
          end
        end
      end
    end
  end
end

이것은 많은 라우트에 대해 이것이 필요한 경우에는 신경을 써줘야 하지만 지금 당장으로는 가장 간단한 해결책일 수 있습니다.

이 접근법은 또한 클래스 메서드에 의존하는 유효성을 정의하는 모델들이 있을 때에도 사용될 수 있습니다. 예를 들어:

# app/models/identity.rb
class Identity < ActiveRecord::Base
  def self.uniqueness_scope
    [:provider]
  end

  prepend_mod_with('Identity')

  validates :extern_uid,
    allow_blank: true,
    uniqueness: { scope: uniqueness_scope, case_sensitive: false }
end

# ee/app/models/ee/identity.rb
module EE
  module Identity
    extend ActiveSupport::Concern

    class_methods do
      extend ::Gitlab::Utils::Override

      def uniqueness_scope
        [*super, :saml_provider_id]
      end
    end
  end
end

이러한 접근 방법을 취하는 대신에, 우리는 우리의 코드를 다음과 같이 리팩토링할 것입니다:

# ee/app/models/ee/identity/uniqueness_scopes.rb
module EE
  module Identity
    module UniquenessScopes
      extend ActiveSupport::Concern

      class_methods do
        extend ::Gitlab::Utils::Override

        def uniqueness_scope
          [*super, :saml_provider_id]
        end
      end
    end
  end
end

# app/models/identity/uniqueness_scopes.rb
class Identity < ActiveRecord::Base
  module UniquenessScopes
    def self.uniqueness_scope
      [:provider]
    end
  end
end

Identity::UniquenessScopes.prepend_mod_with('Identity::UniquenessScopes')

# app/models/identity.rb
class Identity < ActiveRecord::Base
  validates :extern_uid,
    allow_blank: true,
    uniqueness: { scope: Identity::UniquenessScopes.scopes, case_sensitive: false }
end

Code in spec/

EE 전용 기능을 테스트할 때는 기존 CE 스펙에 새로운 예제를 추가하지 말아야 합니다. 또한 EE가 라이선스 없이 실행 될 때 기존 CE 예제를 변경해서는 안 됩니다.

대신 EE 스펙을 ee/spec 폴더에 배치하세요.

Code in spec/factories

기존에 정의된 팩토리를 확장하려면 FactoryBot.modify를 사용하세요.

FactoryBot.modify 블록 내에서 새로운 팩토리(중첩된 팩토리라도)를 정의할 수 없습니다. 아래 예시와 같이 별도의 FactoryBot.define 블록 내에서 이것을 수행할 수 있습니다:

# ee/spec/factories/notes.rb
FactoryBot.modify do
  factory :note do
    trait :on_epic do
      noteable { create(:epic) }
      project nil
    end
  end
end

FactoryBot.define do
  factory :note_on_epic, parent: :note, traits: [:on_epic]
end

Separation of EE code in the frontend

EE 전용 JS 파일을 분리하려면 파일을 ee 폴더로 이동하세요.

예를 들어, app/assets/javascripts/protected_branches/protected_branches_bundle.js와 EE 대응 파일인 ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js가 있을 수 있습니다. 해당 import 문은 다음과 같이 작성될 것입니다:

// app/assets/javascripts/protected_branches/protected_branches_bundle.js
import bundle from '~/protected_branches/protected_branches_bundle.js';

// ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js
// (EE에서만 작동함)
import bundle from 'ee/protected_branches/protected_branches_bundle.js';

// CE: app/assets/javascripts/protected_branches/protected_branches_bundle.js
// EE: ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js
import bundle from 'ee_else_ce/protected_branches/protected_branches_bundle.js';

Add new EE-only features in the frontend

개발 중인 기능이 CE에 없다면, 해당 엔트리 포인트를 ee/에 추가하세요. 예를 들어:

# 마운트할 HTML 엘리먼트 추가
ee/app/views/admin/geo/designs/index.html.haml

# 애플리케이션 초기화
ee/app/assets/javascripts/pages/ee_only_feature/index.js

# 기능 마운트
ee/app/assets/javascripts/ee_only_feature/index.js

licensed_feature_available?License.feature_available?와 같은 기능 보호는 일반적으로 백엔드 가이드에 설명된 대로 컨트롤러에서 수행됩니다.

EE 전용 프론트엔드 기능 테스트

EE 테스트를 추가하려면 CE와 동일한 디렉토리 구조를 따르는 ee/spec/frontend/에 EE 테스트를 추가하세요.

Testing EE-only backend features 아래의 노트를 확인하여 라이선스 기능을 활성화하는 방법을 확인하세요.

CE 기능을 EE 프론트엔드 코드로 확장

기존 뷰를 확장하는 프론트엔드 기능을 보호하기 위해 push_licensed_feature를 사용하여 기존 뷰를 확장하는 프론트엔트 기능을 보호하세요.

# ee/app/controllers/ee/admin/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name) # for global features
end
# ee/app/controllers/ee/group/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name, @group) # for group pages
end
# ee/app/controllers/ee/project/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name, @group) # for group pages
  push_licensed_feature(:my_feature_name, @project) # for project pages
end

브라우저 콘솔에서 gon.licensed_features에 기능이 표시되는지 확인하세요.

Vue 애플리케이션을 EE Vue 컴포넌트로 확장

UI에서 기존 기능을 향상시키는 EE 라이선스 기능은 컴포넌트로서 새로운 요소나 상호 작용을 추가합니다.

CE 컴포넌트 내에서 EE 컴포넌트를 가져와 EE 기능을 추가할 수 있습니다.

EE 컴포넌트를 가져오려면 ee_component 별칭을 사용하세요. EE에서 ee_component 가져오기 별칭은 ee/app/assets/javascripts 디렉토리를 가리킵니다. 반면 CE에서 이 별칭은 아무것도 렌더링하지 않는 빈 컴포넌트로 해결됩니다.

다음은 CE 컴포넌트로 가져온 EE 컴포넌트의 예시입니다.

<script>
// app/assets/javascripts/feature/components/form.vue

// EE에서는 `ee/app/assets/javascripts/feature/components/my_ee_component.vue`로 해결됩니다.
// CE에서는 `app/assets/javascripts/vue_shared/components/empty_component.js`로 해결됩니다.
import MyEeComponent from 'ee_component/feature/components/my_ee_component.vue';

export default {
  components: {
    MyEeComponent,
  },
};
</script>

<template>
  <div>
    <!-- ... -->
    <my-ee-component/>
    <!-- ... -->
  </div>
</template>

참고: EE 컴포넌트는 비동기적으로 가져올 수 있으며, CE 코드베이스 내에서 그 렌더링이 feature flag 체크와 관련이 있는 경우입니다.

glFeatures를 확인하여 Vue 컴포넌트가 보호되었는지 확인하세요. 라이선스가 있을 때에만 컴포넌트가 렌더링됩니다.

<script>
// ee/app/assets/javascripts/feature/components/special_component.vue

import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';

export default {
  mixins: [glFeatureFlagMixin()],
  computed: {
    shouldRenderComponent() {
      // gon.licensed_features에서 `my_feature_name`의 낙타 표기법 버전으로 가져옵니다.
      return this.glFeatures.myFeatureName;
    }
  },
};
</script>

<template>
  <div v-if="shouldRenderComponent">
    <!-- EE 라이선스 기능 UI -->
  </div>
</template>

참고: 필수적인 경우를 제외하고는 mixin을 사용하지 마십시오. 대안을 찾으십시오.

권장되는 대체 방법(네임드/스코프드 슬롯)
  • 우리는 슬롯과/또는 스코프드 슬롯을 사용하여 mixin과 같은 것을 달성할 수 있습니다. EE 컴포넌트만 필요한 경우 CE 컴포넌트를 만들 필요가 없습니다.
  1. 먼저, CE 컴포넌트가 있어야하며, 필요한 경우 CE 템플릿 및 기능이 CE 기본 기능 위에 장식되어야하는 경우 슬롯을 렌더링할 수 있습니다.
// ./ce/my_component.vue

<script>
export default {
  props: {
    tooltipDefaultText: {
      type: String,
    },
  },
  computed: {
    tooltipText() {
      return this.tooltipDefaultText || "5 개의 문제입니다";
    }
  },
}
</script>

<template>
  <span v-gl-tooltip :title="tooltipText" class="ce-text">커뮤니티 에디션 텍스트만</span>
  <slot name="ee-specific-component">
</template>
  1. 다음으로, EE 컴포넌트를 렌더링하고, EE 컴포넌트 내에서 CE 컴포넌트를 렌더링하고 슬롯에 추가 콘텐츠를 추가합니다.
// ./ee/my_component.vue

<script>
export default {
  computed: {
    tooltipText() {
      if (this.weight) {
        return "가중치가 10인 5개의 문제";
      }
    }
  },
  methods: {
    submit() {
      // 무언가 수행.
    }
  },
}
</script>

<template>
  <my-component :tooltipDefaultText="tooltipText">
    <template #ee-specific-component>
      <span class="some-ee-specific">EE 특정 값</span>
      <button @click="submit">클릭하세요</button>
    </template>
  </my-component>
</template>
  1. 마지막으로, 컴포넌트가 필요한 곳에서 다음과 같이 그것을 요구할 수 있습니다.

import MyComponent from 'ee_else_ce/path/my_component'.vue

  • 이렇게 하면 CE 또는 EE 구현에 대해 올바른 컴포넌트가 포함됩니다.

동일한 계산된 값에 대해 다른 결과가 필요한 EE 컴포넌트의 경우 예시에서 볼 수 있듯이 CE 래퍼에 props를 전달할 수 있습니다.

  • EE 추가 HTML
    • EE에 추가 HTML이 있는 템플릿에 대해서는 새 컴포넌트로 이동하고 ee_else_ce 가져오기 별칭을 사용해야 합니다.

다른 JS 코드 확장

JS 파일을 확장하려면 다음 단계를 완료하세요.

  1. EE 전용 코드가 ee/ 폴더 내에 있어야하는 경우 ee_else_ce 헬퍼를 사용하세요.
    1. 전달할 수 없는 함수 내의 코드의 경우, 코드를 새 파일로 이동하고 ee_else_ce 헬퍼를 사용하세요.
  import eeCode from 'ee_else_ce/ee_code';

  function test() {
    const test = 'a';

    eeCode();

    return test;
  }

일부 경우에는 응용 프로그램의 다른 로직을 확장해야 합니다. JS 모듈을 확장하려면 파일의 EE 버전을 만들고 사용자 정의 로직으로 확장하세요.

// app/assets/javascripts/feature/utils.js

export const myFunction = () => {
  // ...
};

// ... 다른 CE 함수들 ...
// ee/app/assets/javascripts/feature/utils.js
import {
  myFunction as ceMyFunction,
} from '~/feature/utils';

/* eslint-disable import/export */

// 동일한 utils를 CE에서 내보냅니다.
export * from '~/feature/utils';

// `myFunction`만 재정의합니다.
export const myFunction = () => {
  const result = ceMyFunction();
  // EE 기능 로직 추가
  return result;
};

/* eslint-enable import/export */

EE/CE 별칭을 사용하여 모듈 테스트하기

프론트엔드 테스트를 작성할 때 테스트 중인 모듈이 ee_else_ce/...로 다른 모듈을 가져오고 해당 테스트에서도 해당 모듈이 필요한 경우, 해당 테스트는 해당 모듈을 ee_else_ce/...로 가져와야합니다. 이렇게 함으로써 예상치 못한 EE 또는 FOSS(자유 오픈 소스 소프트웨어) 실패를 피하고 라이선스가 없을 때 EE가 CE와 같이 동작하도록 돕습니다.

예를 들어:

<script>
// ~/foo/component_under_test.vue

import FriendComponent from 'ee_else_ce/components/friend.vue;'

export default {
  name: 'ComponentUnderTest',
  components: { FriendComponent }.
}
</script>

<template>
  <friend-component />
</template>
// spec/frontend/foo/component_under_test_spec.js

// ...
// ee_else_ce를 사용하여 구성 요소를 참조했기 때문에 spec에서도 동일하게 해야 합니다.
import Friend from 'ee_else_ce/components/friend.vue;'

describe('ComponentUnderTest', () => {
  const findFriend = () => wrapper.find(Friend);

  it('renders friend', () => {
    // 이 코드는 `ee/component...`인 경우 CE에서 실패하고 `~/component...`인 경우 EE에서 실패할 것입니다.
    expect(findFriend().exists()).toBe(true);
  });
});

assets/stylesheets의 SCSS 코드

스타일을 추가하는 구성 요소가 EE에 제한되어 있다면, app/assets/stylesheets 내의 적절한 디렉토리에 별도의 SCSS 파일을 갖는 것이 좋습니다.

일부 경우에는 이것이 완전히 가능하지 않거나 전용 SCSS 파일을 만드는 것이 과한 경우도 있습니다. 예를 들어, 특정 구성 요소의 텍스트 스타일이 EE에서는 다를 수 있습니다. 이러한 경우에는 일반적으로 CE 및 EE 모두에 대해 공통인 스타일시트에 스타일을 유지하고(동일한 것을 설명하는 주석을 추가하여) CE에서 EE로 병합하는 동안 충돌을 피하는 것이 현명합니다.

// 나쁨
.section-body {
  .section-title {
    background: $gl-header-color;
  }

  &.ee-section-body {
    .section-title {
      background: $gl-header-color-cyan;
    }
  }
}
// 좋음
.section-body {
  .section-title {
    background: $gl-header-color;
  }
}

// EE 전용 시작
.section-body.ee-section-body {
  .section-title {
    background: $gl-header-color-cyan;
  }
}
// EE 전용 끝

GitLab-svgs

app/assets/images/icons.json 또는 app/assets/images/icons.svg에서의 충돌은 yarn run svg를 사용하여 이러한 자산을 다시 생성함으로써 해결할 수 있습니다.