기업 에디션 기능 구현 가이드라인

  • ee/에 코드 배치: 모든 기업 에디션 (EE) 코드를 ee/ 최상위 디렉터리에 배치합니다. 다른 코드는 가능한 Community Edition (CE) 파일에 가깝게 유지해야 합니다.
  • 테스트 작성: EE 기능은 모든 코드와 마찬가지로 회귀를 방지하기 위해 좋은 테스트 커버리지가 있어야 합니다. ee/ 코드는 ee/에서 해당 테스트와 함께 있어야 합니다.
  • 문서 작성: doc/ 디렉터리에 문서를 추가합니다. 해당 기능을 설명하고, 적용되는 경우 스크린샷을 포함합니다. 어떤 에디션에 해당하는지 나타냅니다.
  • www-gitlab-com 프로젝트에 MR 제출: 새로운 기능을 EE 기능 디렉터리에 추가합니다.

SaaS 전용 기능

SaaS에만 해당되는 기능을 개발할 때 다음 가이드라인을 사용합니다. (예: CustomersDot 통합)

일반적으로 기능은 SaaS 및 자체 호스팅 배포 모두에 적용되어야 합니다. 그러나 경우에 따라서는 기능이 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 기능을 도입한 MR의 URL입니다.
milestone 아니요 SaaS 기능이 만들어진 마일스톤입니다.
group 아니요 피처 플래그를 소유하는 그룹입니다.

새로운 SaaS 기능 파일 정의 생성

GitLab 코드베이스에는 bin/saas-feature.rb라는 독립적인 도구가 제공되며, 새로운 SaaS 기능 정의를 만들기 위해 사용할 수 있습니다. 이 도구는 새로운 SaaS 기능에 대한 여러 질문을 하고, 그런 다음 ee/config/saas_features에 YAML 정의를 생성합니다.

개발 또는 테스트 환경에서 사용할 수 있는 SaaS 기능 정의 파일만 사용할 수 있습니다.

❯ bin/saas-feature my_saas_feature
그룹 'group::acquisition' 선택

>> SaaS 기능을 도입하는 MR의 URL (스킵하고 MR에서 Danger가 제안하도록 하려면 입력):
?> 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 전용 기능을 활성화하려면 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 'true를 반환합니다' 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 기능을 사용하는 그룹을 실제로 EE 플랜을 사용하도록 확인합니다:

    1. 왼쪽 사이드바에서 하단부에 관리 영역을 선택합니다.
    2. 왼쪽 사이드바에서 개요 > 그룹을 선택합니다.
    3. 수정할 그룹을 식별하고 편집을 선택합니다.
    4. 권한 및 그룹 기능으로 스크롤합니다. 플랜에 대해 Ultimate를 선택합니다.
    5. 변경사항 저장을 선택합니다.

새로운 EE 기능 구현

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

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

가이드로 삼기 위해 다음 질문을 사용하세요:

  1. 새로운 기능인가요, 아니면 기존 라이선스 기능을 확장하는 건가요?
    • 이미 기능이 있는 경우 features.rb를 수정할 필요는 없지만, 기존 기능 식별자를 찾아 보호해야 합니다.
    • 새 기능인 경우 my_feature_name과 같은 식별자를 결정하여 features.rb 파일에 추가하세요.
  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 기능을 볼 수 있음
    

라이선스가 없는 EE 인스턴스에서 CE 인스턴스 시뮬레이션

GitLab CE 기능을 라이선스가없는 EE 인스턴스에서 사용하도록 구현 한 후 GitLab Enterprise Edition은 라이선스가 활성화되지 않은 경우 GitLab Community Edition처럼 작동합니다.

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/<path_to_your_spec>
    

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

기본적으로 개발을위한 머지 요청 파이프라인은 EE 컨텍스트에서만 실행됩니다. FOSS 및 EE 사이에 차이가있는 기능을 개발 중인 경우 FOSS 컨텍스트에서 파이프 라인을 실행하는 것이 좋습니다.

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

자세한 정보는 As-if-FOSS jobs and cross project downstream pipeline 파이프라인 문서를 참조하세요.

백엔드에서 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 경로에있는 모든 경로에 대해 config/application.rb에 동일한 ee/ 접두어 경로를 추가하도록 되어있습니다. 뷰에도 동일한 접두어가 추가됩니다.

EE 전용 백엔드 기능 테스트

CE에 존재하지 않는 EE 클래스를 테스트하려면 보통처럼ee/spec 디렉터리에 특수 파일을 만들되ee/ 하위 디렉터리를 사용하지 마십시오. 예를 들어 ee/app/models/vulnerability.rb 클래스의 경우 ee/spec/models/vulnerability_spec.rb에 테스트를 추가할 수 있습니다.

기본적으로 라이선스 기능은specs/의 사양에 대해 비활성화됩니다. ee/spec 디렉터리의 사양은 기본적으로 Starter 라이선스가 초기화됩니다.

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

  stub_licensed_features(my_awesome_feature_name: true)

CE 기능에 EE 백엔드 코드를 추가

기존 CE 기능을 기반으로 하는 기능에 대해 EE 네임 스페이스에 모듈을 작성하고 해당 모듈을 CE 클래스에 주입합니다. 이렇게하면 CE에서 EE로 Merge하는 경우 충돌이 덜 발생할 가능성이 더 높아집니다. CE 클래스에 추가되는 유일한 줄, 즉 모듈을 주입하는 줄은 파일의 마지막 줄에 있어야합니다. 예를 들어 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/ 하위 디렉터리에 있어야합니다. 예를 들어, 우리가 user 모델에 모듈을 추가하려는 경우 ee/app/models/ee/user.rb 내부에 ::EE::User라는 모듈을 만들어야합니다.

이는 모델에만 해당되는 것이 아니며 다음과 같은 다른 예시가 있습니다:

  • 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
  • ee/app/views/ee/foo.html.haml
  • ee/app/views/ee/foo/_bar.html.haml

CE 기능을 기반으로 한 EE 기능 테스트

CE 클래스에 EE 기능을 확장하는 EE 네임스페이스 모듈을 테스트하려면 보통대로 ee/spec 디렉터리에 스펙 파일을 만들어야 합니다. 두 번째 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 기능'
end

CE 메서드 재정의

CE 코드베이스에 있는 메서드를 재정의하려면 prepend를 사용하세요. 이를 통해 모듈에서 클래스의 메서드를 재정의하고 super를 통해 클래스의 구현에 여전히 접근할 수 있습니다.

이에 대한 몇 가지 유의 사항이 있습니다:

  • 항상 extend ::Gitlab::Utils::Override를 사용하고 override를 사용하여 overrider 메서드를 보호하여 CE에서 메서드의 이름이 변경되어도 EE 재정의가 무시되지 않도록 합니다.
  • overrider가 CE 구현 중간에 라인을 추가하는 경우, CE 메서드를 리팩토링하고 더 작은 메서드로 분할해야 합니다. 또는 CE에서 비어 있는 “hook” 메서드를 만들거나, EE별 구현과 함께 있는 “hook” 메서드를 만들어야 합니다.
  • 원래 구현에 가드 절(예: return unless condition)이 포함된 경우, 메서드를 재정의하여 행위를 확장하는 것이 쉽지 않을 수 있습니다. 왜냐하면 재정의된 메서드(즉, 재정의된 메서드에서 super를 호출하는 것)가 일찍 중단되어야 하는 시점을 알 수 없기 때문입니다. 이 경우, 단순히 일반 메서드를 재정의하는 것이 아니라, 원래 메서드를 업데이트하고 다른 방법을 확장하도록 만들어야 합니다. 예를 들어, 기본으로 주어진 다음의 경우: ```ruby class Base def execute return unless enabled?

      # ...
      # ...
    end   end ```
    

    Base#execute를 단순히 재정의하는 대신, 업데이트하고 다른 방법에 대한 호출을 추출해야 합니다: ```ruby class Base def execute return unless enabled?

      do_something
    end
      
    private
      
    def do_something
      # ...
      # ...
    end   end ```
    

    그럼 우리는 가드들을 걱정하지 않고 do_something을 재정의할 수 있습니다: ```ruby module EE::Base extend ::Gitlab::Utils::Override

    override :do_something
    def do_something
      # 위의 패턴을 따라서 super를 호출하고 확장합니다
    end   end ```
    

Tier: Free, Premium, Ultimate

Offering: GitLab.com, Self-Managed

render_if_exists 사용

일반 render 대신에 render_if_exists를 사용해야 합니다. 이는 특정 부분을 찾을 수 없을 경우 아무것도 렌더링하지 않습니다. CE와 EE 간에 코드를 동일하게 유지하기 위해 render_if_exists를 사용합니다.

이로 인한 이점:

  • 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 부분에 뭔가를 추가하고 싶을 수 있기 때문에 CE 부분을 EE 부분에서 재사용하고 싶을 수 있습니다. 이를 위해 다른 이름의 부분을 추가하는 대신 노력해야 하지만 이는 지루할 수 있습니다.

이 경우, 모든 EE 부분을 무시하는 render_ce를 사용할 수 있습니다. 예를 들어 다음과 같습니다.

ee/app/views/projects/settings/_archive.html.haml:

- return if @project.marked_for_deletion?
= render_ce 'projects/settings/archive'

위의 예시에서 render 'projects/settings/archive'를 사용할 수 없습니다. 왜냐하면 동일한 EE 부분을 찾아 무한 재귀가 발생하기 때문입니다. 대신, render_ce를 사용하여 ee/ 내의 부분을 무시한 다음, 동일한 경로(projects/settings/archive)에 대해 CE 부분을 렌더링할 수 있습니다. 이를 통해 CE 부분을 쉽게 감쌀 수 있습니다.

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 뮤테이션, 리졸버 또는 타입을 재정의하려면 ee/app/graphql/ee/{mutations,resolvers,types}에 파일을 생성하고 prepended 블록에 새 코드를 추가하세요.

예를 들어, CE에 Mutations::Tanukis::Create라는 뮤테이션이 있고 새 인수를 추가하려면, EE에서 CE 오버라이드를 다음 위치에 추가하세요.

ee/app/graphql/ee/mutations/tanukis/create.rb:

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/의 코드

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

CE 기능을 EE 백엔드 코드로 확장에 따라 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 매개변수

params를 정의하고 이를 사용하여 EE에서 오버라이드할 수 있도록 인터페이스를 먼저 정의해야 합니다. 다른 곳에서는 이를 해야할 필요가 없지만 Grape는 내부적으로 복잡하므로 일반 객체 지향적인 프랙티스를 따르는 것이 좋습니다.

예를 들어, 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 모듈에서 이를 오버라이드할 수 있습니다:

module EE
  module API
    module Helpers
      module ProjectsHelpers
        extend ActiveSupport::Concern
        
        prepended do
          params :optional_project_params_ee do
            # EE 특정 매개변수가 여기에 있음...
          end
        end
      end
    end
  end
end

EE 도우미

EE 모듈이 CE 도우미를 덮어쓸 수 있도록하기 쉽게하려면, 확장하려는 도우미를 먼저 정의해야 합니다. 쉽고 명확하게 하기 위해 클래스 정의 바로 뒤에 바로 하는 것이 좋습니다.

module API
  module Ci
    class JobArtifacts < Grape::API::Instance
      # EE::API::Ci::JobArtifacts가 다음 도우미들을 덮어쓸 것입니다
      helpers do
        def authorize_download_artifacts!
          authorize_read_builds!
        end
      end
    end
  end
end

API::Ci::JobArtifacts.prepend_mod_with('API::Ci::JobArtifacts')

그런 다음 우리는 정규 객체 지향 관행을 따를 수 있습니다:

module EE
  module API
    module Ci
      module JobArtifacts
        extend ActiveSupport::Concern
        
        prepended do
          helpers do
            def authorize_download_artifacts!
              super
              check_cross_project_pipelines_feature!
            end
          end
        end
      end
    end
  end
end

EE 특정 동작

가끔은 API 중 일부에서 EE 특정 동작이 필요합니다. 보통 CE 메서드를 덮어쓰기 위해 EE 메소드를 사용할 수 있지만, API 루트들은 메서드가 아니기 때문에 덮어쓸 수 없습니다. CE 루트에 동작을 주입할 “훅”이나 독립적인 메소드로 추출 할 필요가 있습니다.

module API
  class MergeRequests < Grape::API::Instance
    helpers do
      # EE::API::MergeRequests가 다음 도우미들을 덮어쓸 것입니다
      def update_merge_request_ee(merge_request)
      end
    end
    
    put ':id/merge_requests/:merge_request_iid/merge' do
      merge_request = find_project_merge_request(params[:merge_request_iid])
      
      # ...
      
      update_merge_request_ee(merge_request)
      
      # ...
    end
  end
end

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

update_merge_request_ee가 CE에서는 아무것도 하지 않지만, 그럼에도 불구하고 우리는 EE에서 덮어쓸 수 있습니다.

module EE
  module API
    module MergeRequests
      extend ActiveSupport::Concern
      
      prepended do
        helpers do
          def update_merge_request_ee(merge_request)
            # ...
          end
        end
      end
    end
  end
end

EE route_setting

EE 모듈에서 이것을 확장하는 것은 매우 어렵습니다. 이것은 특정 루트의 메타데이터를 저장하는 것입니다. 그러므로 우리는 그것이 CE에서는 상관이 없기 때문에 EE route_setting을 CE에 두어도 괜찮습니다.

우리가 route_setting을 더 자주 사용하고 진정으로 EE에서 확장해야 하는지 여부를 재고할 수 있을 때 이 정책을 다시 검토할 수 있습니다. 현재로서는 그것을 잘 사용하지 않기 때문에 문제가 되지 않습니다.

EE 특정 데이터 설정을 위한 클래스 메소드 활용

특정 API 루트에 대해 다른 인수를 사용해야 하는 경우가 있고, Grape는 서로 다른 컨텍스트를 가지고 있기 때문에 우리는 쉽게 EE 모듈로 확장하기 어렵습니다. 이를 극복하기 위해 데이터를 별도의 모듈이나 클래스에 저장하는 클래스 메소드로 이동해야 합니다. 이렇게하면 데이터가 사용되기 전에 해당 모듈이나 클래스를 확장할 수 있으므로 CE 코드 중간에 prepend_mod_with를 놓을 필요가 없어집니다.

예를 들어, at_least_one_of에 추가적인 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

spec/에 있는 코드

EE 전용 기능을 테스트할 때는 기존 CE 스펙에 새로운 예제를 추가하지 않도록하십시오. 또한 EE가 라이선스없이 실행될 때 기존 CE 예제가 여전히 작동해야 하므로 기존 CE 예제를 수정하지 마십시오.

대신에 EE 스펙을 ee/spec 폴더에 두십시오.

spec/factories에 있는 코드

이미 CE에서 정의된 팩토리를 확장하려면 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

프론트엔드에서의 EE 코드 분리

EE-특정 JS 파일을 분리하려면 파일을 ee 폴더로 이동하십시오.

예를 들어, app/assets/javascripts/protected_branches/protected_branches_bundle.js와 EE 대응파일인ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js이 있을 수 있습니다. 해당 가져오기 문은 다음과 같아야 합니다:

// 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
// (only works in EE)
import bundle from 'ee/protected_branches/protected_branches_bundle.js';

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

Frontend에 새로운 EE 전용 기능 추가

개발 중인 기능이 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/spec/frontend/에 EE 테스트를 추가하십시오.

라이선스 기능을 활성화하는 데 관한 EE 전용 백엔드 기능 테스트 아래의 참고사항을 확인하십시오.

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

기존 뷰를 확장하는 프론트엔드 기능을 가드하는 데 push_licensed_feature를 사용하십시오.

# ee/app/controllers/ee/admin/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name) # 글로벌 기능의 경우
end
# ee/app/controllers/ee/group/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name, @group) # 그룹 페이지의 경우
end
# ee/app/controllers/ee/project/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name, @group) # 그룹 페이지의 경우
  push_licensed_feature(:my_feature_name, @project) # 프로젝트 페이지의 경우
end

브라우저 콘솔에서 gon.licensed_features에 기능이 나타나는지 확인하십시오.

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

UI에서 기능을 강화하는 EE 라이선스 기능은 컴포넌트로서 새로운 요소나 상호 작용을 Vue 애플리케이션에 추가합니다.

템플릿 차이를 분리하기 위해 CE 템플릿과 다르게 사용하려면, Vue 템플릿 차이를 분리하기 위해 자식 EE 컴포넌트를 사용하십시오. 이는 CE 리포지터리에 있어야 할 코드입니다. EE에서는 이 코드가 올바른 컴포넌트를 로드하도록 허용하며, CE에서는 아무것도 렌더링하지 않는 빈 컴포넌트를 로드합니다.

CE 컴포넌트는 EE 기능의 엔트리 포인트로 작용합니다. EE 컴포넌트를 추가하려면 ee/ 디렉터리를 찾아서 import('ee_component/...')로 추가하십시오.

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

export default {
  mixins: [glFeatureFlagMixin()],
  components: {
    // CE에서 EE 컴포넌트 가져오기
    MyEeComponent: () => import('ee_component/components/my_ee_component.vue'),
  },
};
</script>

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

Vue 컴포넌트가 가드되는지 확인하기 위해 glFeatures를 확인하십시오. 라이선스가 있는 경우에만 컴포넌트를 렌더링합니다.

<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() {
      // `my_feature_name`의 캐멀 케이스로 표시된 것으로 `gon.licensed_features`에서 가져옴
      return this.glFeatures.myFeatureName;
    }
  },
};
</script>

<template>
  <div v-if="shouldRenderComponent">
    <!-- EE 라이선스 기능 UI -->
  </div>
</template>
note
미적스를 사용하는 것은 최후의 수단일 때에만 사용하십시오. 대안 방법을 찾아보십시오.
권장하는 대안 접근 방식 (named/scoped slots)
  • 우리는 믹스인과 같은 작업을 수행하는 때에만 슬롯과/또는 스코프드 슬롯을 사용할 수 있습니다. EE 컴포넌트가 필요한 경우에는 CE 컴포넌트를 만들 필요가 없습니다.
  1. 먼저, CE 컴포넌트를 만들어 CE 기본에 EE 템플릿과 기능을 데코레이트해야 하는 경우 슬롯을 렌더링할 수 있습니다.
// ./ce/my_component.vue

<script>
export default {
  props: {
    tooltipDefaultText: {
    type: String,
    },
  },
  computed: {
    tooltipText() {
      return this.tooltipDefaultText || "5 issues please";
    }
  },
}
</script>

<template>
  <span v-gl-tooltip :title="tooltipText" class="ce-text">Community Edition Only Text</span>
  <slot name="ee-specific-component">
</template>
  1. 다음으로, EE 컴포넌트를 렌더링하고, 내부에서 CE 컴포넌트를 렌더링하고 슬롯에 추가적인 내용을 더할 수 있습니다.
// ./ee/my_component.vue

<script>
export default {
  computed: {
    tooltipText() {
      if (this.weight) {
        return "5 issues with weight 10";
      }
    }
  },
  methods: {
    submit() {
      // 무엇인가를 수행합니다.
    }
  },
}
</script>

<template>
  <my-component :tooltipDefaultText="tooltipText">
    <template #ee-specific-component>
      <span class="some-ee-specific">EE Specific Value</span>
      <button @click="submit">Click Me</button>
    </template>
  </my-component>
</template>
  1. 마지막으로, 컴포넌트가 필요한 곳에서 다음과 같이 필요로 하는 파일을 가져올 수 있습니다.

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

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

같은 계산된 값에 대해 다른 결과가 필요한 EE 컴포넌트의 경우, 앞서 설명된 예제처럼 CE 래퍼에 props를 전달할 수 있습니다.

  • EE 하위 컴포넌트
    • 어떤 컴포넌트를 로드할지 확인하기 위해 비동기 로딩을 사용하기 때문에, 다음 예제를 확인하십시오.
  • EE 추가 HTML
    • EE에 추가 HTML가 있는 템플릿의 경우, 새로운 컴포넌트로 이동하고 ee_else_ce 동적 가져오기를 사용하십시오.

다른 JS 코드 확장

JS 파일을 확장하려면 다음 단계를 따르십시오.

  1. ee_else_ce 도우미를 사용하여 EE 전용 코드를 ee/ 폴더 안에 배치하세요.
    1. EE 전용 코드만 포함된 EE 파일을 만들고 CE와 확장하세요.
    2. 확장할 수 없는 함수 내 코드의 경우, 코드를 새 파일로 이동하고 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 */

// 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 또는 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`를 사용하여 참조했기 때문에, 테스트에서도 동일한 방식으로 가져와야 합니다.
import Friend from 'ee_else_ce/components/friend.vue;'

describe('ComponentUnderTest', () => {
  const findFriend = () => wrapper.find(Friend);
  
  it('친구를 렌더링합니다', () => {
    // CE에서는 `ee/component...`을 사용한 경우에 실패하며,
    // EE에서는 `~/component...`을 사용한 경우에 실패합니다.
    expect(findFriend().exists()).toBe(true);
  });
});

assets/stylesheets의 SCSS 코드

스타일을 추가하는 컴포넌트가 EE에 제한된 경우, app/assets/stylesheets 내의 적절한 디렉터리에 별도의 SCSS 파일을 가지는 것이 좋습니다.

일부 경우에는 이것이 완전히 가능하지 않거나 전용 SCSS 파일을 만드는 것이 지나칠 수 있습니다. 예를 들어, 특정 컴포넌트의 텍스트 스타일이 EE에 대해 다를 경우가 그 예입니다. 이러한 경우에는 일반적으로 CE와 EE 모두에 대해 공통인 스타일시트에 스타일을 유지하고, 이와 같은 경우에 CE 규칙과 겹치지 않도록 (동일한 내용을 설명하는 주석을 추가) 해당 규칙을 분리하는 것이 현명합니다.

// 나쁨
.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로 해당 에셋을 다시 생성하여 해결할 수 있습니다.