Enterprise Edition 기능 구현 가이드라인
GitLab v19.1코드를 ee/에 배치: 모든 Enterprise Edition(EE) 코드는 ee/ 최상위 디렉터리 안에 넣으세요. 테스트 작성: 다른 코드와 마찬가지로, EE 기능은 회귀를 방지하기 위해 충분한 테스트 커버리지를 가져야 합니다.
-
코드를
ee/에 배치: 모든 Enterprise Edition(EE) 코드는ee/최상위 디렉터리 안에 넣으세요. 나머지 코드는 Community Edition(CE) 파일과 최대한 가깝게 유지해야 합니다. -
테스트 작성: 다른 코드와 마찬가지로, EE 기능은 회귀를 방지하기 위해 충분한 테스트 커버리지를 가져야 합니다. 모든
ee/코드는ee/에 대응하는 테스트가 있어야 합니다. -
문서 작성:
doc/디렉터리에 문서를 추가하세요. 기능을 설명하고, 해당되는 경우 스크린샷을 포함하세요. 기능이 적용되는 에디션을 명시하세요. -
www-gitlab-com프로젝트에 MR 제출: 새로운 기능을 EE 기능 목록에 추가하세요.
개발 환경에서의 런타임 모드#
-
EE 언라이선스: 메인 리포지터리에서 GDK를 일반적으로 설치했을 때 기본 상태입니다.
-
EE 라이선스: GDK에 유효한 라이선스를 추가했을 때입니다.
-
GitLab.com: SaaS를 시뮬레이션할 때입니다.
-
CE: 위의 상태 중 어느 것이든, CE를 시뮬레이션할 때입니다.
기능 구현 의사결정 흐름#
다음 다이어그램은 CE/EE/SaaS/Dedicated 레이어에 걸쳐 기능을 어디서 어떻게 구현할지 결정하는 방법을 보여줍니다:
%%{init: { "fontFamily": "GitLab Sans" }}%% flowchart TD accTitle: Feature implementation decision flow accDescr: Diagram showing how to decide where and how to implement features across CE/EE/SaaS/Dedicated layers
A[Developer wants to implement a feature] --> B{What type of feature?}
B -->|CE Feature| C[Implement in main codebase]
B -->|EE Licensed Feature| D[EE Feature Path]
B -->|SaaS-only Feature| E[SaaS Feature Path]
B -->|Dedicated Feature| F[Dedicated Feature Path]
C --> C1[Place code in app/, lib/, etc.]
C --> C2[Write tests in spec/]
C --> C3[No license checks needed]
D --> D1{New or extending existing?}
D1 -->|New EE Feature| D2[Place in ee/ directory]
D1 -->|Extending CE| D3[Create EE module with prepend_mod]
D2 --> D4[Add to ee/app/models/gitlab_subscriptions/features.rb]
D3 --> D4
D4 --> D5{Which plan?}
D5 -->|Premium| D6[Add to PREMIUM_FEATURES]
D5 -->|Ultimate| D7[Add to ULTIMATE_FEATURES]
D5 -->|Global/Instance| D8[Add to GLOBAL_FEATURES]
D6 --> D9[Guard with project.licensed_feature_available?]
D7 --> D9
D8 --> D10[Guard with License.feature_available?]
D9 --> D11[Write tests in ee/spec/]
D10 --> D11
D11 --> D12[Use stub_licensed_features in tests]
E --> E1[Add feature to FEATURES in ee/lib/ee/gitlab/saas.rb]
E1 --> E2[Create YAML definition in ee/config/saas_features/]
E2 --> E3[Use bin/saas-feature.rb tool]
E3 --> E4[Guard with Gitlab::Saas.feature_available?]
E4 --> E5{Extending CE feature?}
E5 -->|Yes| E6[Create EE module that extends CE]
E5 -->|No| E7[Create new EE-only code]
E6 --> E8[Use prepend_mod pattern]
E7 --> E9[Place directly in ee/ directory]
E8 --> E10[Write tests in ee/spec/]
E9 --> E10
E10 --> E11[Use stub_saas_features helper]
F --> F1[Add to FEATURES in ee/lib/gitlab/dedicated.rb]
F1 --> F2[Create YAML definition with bin/dedicated-feature.rb]
F2 --> F3{Extending CE feature?}
F3 -->|Yes| F4[Create EE module that extends CE]
F3 -->|No| F5[Create new EE-only code]
F4 --> F6[Use prepend_mod pattern]
F5 --> F7[Place directly in ee/ directory]
F6 --> F8[Guard with Gitlab::Dedicated.feature_available?]
F7 --> F8
F8 --> F9[Write tests in ee/spec/]
이 다이어그램은 네 가지 주요 구현 레이어를 보여줍니다:
-
CE (녹색): 라이선스 요구 사항이 없는 Community Edition 기능. 타깃 사용자가 GitLab.com의 무료 사용자라면 SaaS 의사결정 경로를 따르세요.
-
EE (주황색): Premium/Ultimate 라이선스가 필요한 Enterprise Edition 기능
-
SaaS (분홍색): GitLab.com 인스턴스 전용 기능
-
Dedicated (파란색): GitLab Dedicated 인스턴스에서 다르게 동작하는 기능
주요 의사결정 포인트:
-
파일 배치: CE 코드는 메인 디렉터리에, EE 코드는
ee/하위 디렉터리에 배치 -
기능 가드: 각 레이어마다 다른 메서드 사용 (
licensed_feature_available?,License.feature_available?,Gitlab::Saas.feature_available?,Gitlab::Dedicated.feature_available?) -
테스트 접근 방식: 각 레이어마다 테스트를 위한 특정 헬퍼와 메타데이터 존재
SaaS 전용 기능#
SaaS에만 해당하는 기능(예: CustomersDot 통합)을 개발할 때 다음 가이드라인을 사용하세요.
일반적으로 기능은 SaaS와 Self-managed 배포 모두에서 제공되어야 합니다. 그러나 SaaS에서만 사용 가능해야 하는 경우도 있으며, 이 가이드는 그 방법을 설명합니다.
Gitlab::Saas.feature_available? 사용을 권장합니다. 이를 통해 기능이 SaaS 전용인 이유를 풍부한 컨텍스트로 정의할 수 있습니다.
Gitlab::Saas.feature_available?로 SaaS 전용 기능 구현하기#
FEATURES 상수에 추가하기#
새로운 SaaS 전용 기능의 이름 지정을 위해 네임스페이싱 개념 가이드를 참조하세요.
ee/lib/gitlab/saas.rb의 FEATURE에 새 기능을 추가하세요.
FEATURES = %i[purchases_additional_minutes some_domain_new_feature_name].freeze
코드에서 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.rb my_saas_feature
You picked the group 'group::acquisition'
>> URL of the MR introducing the SaaS feature (enter to skip and let Danger provide a suggestion directly in the 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/gitlab/saas.rb 클래스를 prepend하고 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 전용 기능의 영향을 받는 모든 코드에 대해 기능이 활성화된 경우와 비활성화된 경우 모두 자동화 테스트를 포함하세요.
애플리케이션 코드에서 Gitlab.com? 대신 Gitlab::Saas.feature_available?(:specific_feature)를 사용하여 무언가가 SaaS 전용인 이유를 전달하는 것처럼, 같은 이유로 테스트에서도 특정 SaaS 기능 메타데이터 태그를 사용해야 합니다.
이를 통해 기능 구현과 테스트 사이에 명확한 연결이 생성되어 코드베이스의 유지 보수성과 자체 문서화가 향상됩니다.
SaaS 기능 메타데이터 태그 사용 (권장)#
대부분의 테스트 시나리오에서는 메타데이터 태그를 사용하여 stub_saas_features를 수동으로 호출하지 않고 SaaS 기능을 자동으로 활성화하세요. 이 접근 방식은 통합 테스트나 전체 테스트 컨텍스트에 SaaS 기능을 활성화해야 할 때 특히 유용합니다.
테스트 컨텍스트 또는 개별 예제에 saas_를 앞에 붙인 SaaS 기능 이름을 메타데이터로 추가하세요:
# Context-level metadata (applies to all examples in the context)
describe 'some feature', :saas_gitlab_com_subscriptions do
it 'shows SaaS-specific functionality' do
expect(page).to have_content('SaaS Feature')
end
end
# Individual example metadata
describe 'some feature' do
it 'shows SaaS-specific functionality', :saas_gitlab_com_subscriptions do
expect(page).to have_content('SaaS Feature')
end
it 'works without SaaS features' do
expect(page).not_to have_content('SaaS Feature')
end
end
# Multiple SaaS features
context 'with multiple SaaS features', :saas_onboarding, :saas_gitlab_com_subscriptions do
# Both 'onboarding' and 'duo_enterprise' features are enabled
end
이 메타데이터 접근 방식은:
-
태그된 각 기능에 대해 자동으로
stub_saas_features(feature_name: true)를 호출합니다. -
컨텍스트 수준(describe/context 블록)과 개별 예제 수준(it 블록) 모두에서 작동합니다.
-
Gitlab::Saas::FEATURES에 정의된 모든 SaaS 기능과 함께 작동합니다. -
before블록에서stub_saas_features를 수동으로 호출하는 것보다 더 깔끔합니다.
테스트 컨텍스트나 특정 예제에서 SaaS 기능을 활성화해야 할 때 이 접근 방식을 사용하세요. 더 세밀한 제어가 필요하거나 같은 예제 내에서 활성화/비활성화 상태 모두를 테스트할 때는 stub_saas_features 헬퍼를 직접 계속 사용하세요.
stub_saas_features 헬퍼 사용 (고급 시나리오)#
기능 상태에 대한 세밀한 제어가 필요하거나 같은 테스트 내에서 활성화/비활성화 경로를 모두 테스트해야 하는 복잡한 시나리오에서는 stub_saas_features 헬퍼를 직접 사용하세요.
테스트에서 SaaS 전용 기능을 활성화하려면 stub_saas_features 헬퍼를 사용하세요:
stub_saas_features(purchases_additional_minutes: true)
::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
두 경로를 모두 테스트하는 일반적인 패턴은 다음과 같습니다:
it 'purchases/additional_minutes is not available by default' do
# tests assuming purchases_additional_minutes is not enabled by default
::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
end
context 'when purchases_additional_minutes is available' do
before do
stub_saas_features(purchases_additional_minutes: true)
end
it 'returns true' do
::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
end
end
:saas 메타데이터 헬퍼 사용 (특정 시나리오)#
:saas 메타데이터 헬퍼는 코드가 특정 SaaS 기능 대신 Gitlab.com? 접근 방식에 의존하는 특정 시나리오에서 사용해야 합니다. 여기에는 다음이 포함됩니다:
-
아직 특정 SaaS 기능을 사용하도록 변환되지 않은 코드
-
데이터베이스 마이그레이션과 같이
Gitlab.com?체크가 적절한 접근 방식인 영역 (SaaS 기능 패턴의 예외로서)
새로운 SaaS 전용 기능의 경우 SaaS 기능 메타데이터 태그를 사용하세요.
테스트에 대한 자세한 내용은 SaaS에 의존하는 테스트를 참조하세요.
스펙에서의 사용 예시:
# spec/migrations/20240510113339_add_saas_specific_column_spec.rb
RSpec.describe AddSaasSpecificColumn do
it 'adds column for self-managed instances' do
migrate!
expect(table(:projects)).to have_column(:some_column)
end
context 'when SaaS', :saas do
it 'adds additional SaaS-specific column' do
migrate!
expect(table(:projects)).to have_column(:some_column)
expect(table(:projects)).to have_column(:saas_specific_column)
end
end
end
SaaS 인스턴스 시뮬레이션#
로컬에서 개발 중이고 인스턴스가 SaaS(GitLab.com) 버전의 제품을 시뮬레이션해야 하는 경우:
다음 환경 변수를 내보내세요:
export GITLAB_SIMULATE_SAAS=1
로컬 GitLab 인스턴스에 환경 변수를 전달하는 방법은 여러 가지가 있습니다.
예를 들어, gdk.yml 파일에 항목을 만들 수 있습니다.
라이선스된 EE 기능 사용 허용을 활성화하여 프로젝트 네임스페이스의 플랜에 기능이 포함된 경우에만 프로젝트에서 라이선스된 EE 기능을 사용할 수 있도록 합니다.
오른쪽 상단에서 Admin을 선택하세요.
-
왼쪽 사이드바에서 Settings > General을 선택하세요.
-
Account and limit을 확장하세요.
-
Allow use of licensed EE features 체크박스를 선택하세요.
-
Save changes를 선택하세요.
EE 기능을 테스트하려는 그룹이 실제로 EE 플랜을 사용하고 있는지 확인하세요:
오른쪽 상단에서 Admin을 선택하세요.
-
왼쪽 사이드바에서 Overview > Groups를 선택하세요.
-
수정하려는 그룹을 찾아 Edit를 선택하세요.
-
Permissions and group features로 스크롤합니다. Plan에서
Ultimate를 선택하세요. -
Save changes를 선택하세요.
위 단계를 수행하는 방법을 보여주는 📺 동영상이 있습니다.
Dedicated 인스턴스 기능#
코드에서 GitLab Dedicated 인스턴스를 다르게 처리해야 할 때 다음 가이드라인을 사용하세요.
GitLab Dedicated 인스턴스는 항상 Dedicated 아키텍처에 문서화된 대로 Ultimate 티어로 프로비저닝됩니다. Dedicated는 전적으로 Enterprise Edition 제공 사항이므로, Dedicated 전용 코드는 모두 다른 EE 기능과 동일한 패턴을 따라 ee/ 디렉터리 구조에 배치해야 합니다.
일반적인 사용 사례#
Dedicated 전용 코드는 Dedicated 인스턴스에서만 사용 가능해야 하는 기능을 위한 것입니다.
일반적으로 기능은 SaaS와 Self-managed 배포 모두에서 제공되어야 합니다. 그러나 Dedicated 전용 기능에 대한 유효한 사례도 있습니다.
Gitlab::Dedicated 메서드 사용#
Gitlab::Dedicated 모듈은 Dedicated 전용 동작을 처리하기 위한 feature_available? 메서드를 제공합니다:
기능이 Dedicated에서만 실행되어야 할 때 FEATURES 목록과 함께 feature_available?를 사용하세요:
return unless Gitlab::Dedicated.feature_available?(:custom_backup_strategy)
# Custom backup code that only runs on Dedicated
ee/lib/gitlab/dedicated.rb의 FEATURES에 기능을 추가하세요:
FEATURES = %i[custom_backup_strategy skip_ultimate_trial_experience].freeze
feature_available? 메서드는 ee/config/dedicated_features/의 YAML 파일을 통해 컨텍스트가 풍부한 정의를 가능하게 하여 기능이 Dedicated 인스턴스에서 다르게 동작하는 이유를 문서화합니다.
Dedicated 기능 정의 및 검증#
이 프로세스는 코드베이스에서 Dedicated 기능을 일관되게 사용하도록 보장합니다. 모든 Dedicated 기능은 반드시:
-
알려져 있어야 합니다.
FEATURES에 명시적으로 정의된 Dedicated 기능만 사용하세요. -
소유자가 있어야 합니다.
모든 Dedicated 기능은 다음 위치에 저장된 YAML 파일에 자체 문서화됩니다:
각 Dedicated 기능은 여러 필드로 구성된 별도의 YAML 파일에 정의됩니다:
| 필드 | 필수 | 설명 |
|---|---|---|
| name | 예 | Dedicated 기능의 이름. |
| introduced_by_url | 아니오 | Dedicated 기능을 도입한 머지 리퀘스트의 URL. |
| milestone | 아니오 | Dedicated 기능이 생성된 마일스톤. |
| group | 아니오 | 기능을 소유하는 그룹. |
새 Dedicated 기능 파일 정의 만들기#
GitLab 코드베이스는 새 Dedicated 기능 정의를 만들기 위한 도구인 bin/dedicated-feature.rb를 제공합니다.
이 도구는 새 Dedicated 기능에 대한 다양한 질문을 한 후 ee/config/dedicated_features에 YAML 정의를 생성합니다.
YAML 정의 파일이 있는 Dedicated 기능만 개발 또는 테스트 환경에서 사용할 수 있습니다.
새 Dedicated 기능 정의를 만들려면:
-
기능 이름 지정을 위해 네임스페이싱 개념 가이드를 참조하세요.
-
ee/lib/gitlab/dedicated.rb의FEATURES에 기능을 추가하세요. -
bin/dedicated-feature.rb <feature-name>을 실행하여ee/config/dedicated_features/에 YAML 정의를 만드세요.
실행 예시:
❯ bin/dedicated-feature.rb my_dedicated_feature
You picked the group 'group::acquisition'
>> URL of the MR introducing the Dedicated feature (enter to skip and let Danger provide a suggestion directly in the MR):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123456
create ee/config/dedicated_features/my_dedicated_feature.yml
---
name: my_dedicated_feature
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123456
milestone: '19.1'
group: group::acquisition
Dedicated 코드가 ee/에 있어야 하는 이유#
모든 Dedicated 전용 코드는 ee/ 디렉터리 구조에 배치해야 합니다. 이를 통해 다음이 보장됩니다:
-
Dedicated 기능은 EE 빌드에서만 사용 가능합니다.
-
코드베이스가 CE와 EE 기능 간 명확한 분리를 유지합니다.
-
Dedicated 인스턴스는 모든 Ultimate 기능과 Dedicated 전용 동작에 접근할 수 있습니다.
SaaS 전용 기능과 마찬가지로, 애플리케이션 코드에서 Gitlab::CurrentSettings.gitlab_dedicated_instance?를 직접 사용하지 마세요. 대신 Gitlab::Dedicated.feature_available?(:specific_feature)를 사용하여 기능이 Dedicated에서 다르게 동작하는 이유를 제공하세요.
Gitlab/AvoidGitlabDedicatedInstanceChecks RuboCop 규칙은 RuboCop 구성에서 명시적으로 제외된 경우를 제외하고 Gitlab::CurrentSettings.gitlab_dedicated_instance? 및 Gitlab::Dedicated.dedicated_instance?에 대한 직접 호출에 플래그를 지정하여 이 규약을 강제합니다.
데이터베이스 마이그레이션의 예외#
데이터베이스 마이그레이션은 마이그레이션이 배포 유형에 따라 다르게 동작해야 할 때 Gitlab::CurrentSettings.gitlab_dedicated_instance?를 사용하여 Dedicated 인스턴스를 확인해야 할 수 있습니다. 이는 Gitlab::Dedicated.feature_available? 패턴을 사용할 수 없는 마이그레이션에서 허용됩니다.
새 EE 기능 구현#
GitLab Premium 또는 GitLab Ultimate 라이선스 기능을 개발하는 경우, 다음 단계를 사용하여 새 기능을 추가하거나 확장하세요.
GitLab 라이선스 기능은 ee/app/models/gitlab_subscriptions/features.rb에 추가됩니다. 이 파일을 수정하는 방법을 결정하려면 먼저 제품 관리자와 기능이 라이선싱에 어떻게 맞는지 논의하세요.
다음 질문을 참고하세요:
- 이것은 새 기능인가요, 아니면 기존 라이선스 기능을 확장하는 것인가요?
기능이 이미 존재한다면 features.rb를 수정할 필요가 없지만, 가드를 위해 기존 기능 식별자를 찾아야 합니다.
-
새 기능이라면
features.rb파일에 추가할my_feature_name과 같은 식별자를 결정하세요. -
이것은 GitLab Premium 기능인가요, 아니면 GitLab Ultimate 기능인가요?
선택한 플랜에 따라 PREMIUM_FEATURES 또는 ULTIMATE_FEATURES에 기능 식별자를 추가하세요.
- 이 기능은 전역적으로(GitLab 인스턴스 전체에) 사용 가능한가요?
Geo 및
데이터베이스 로드 밸런싱과 같은 기능은 전체 인스턴스에서 사용되며 개별 사용자 네임스페이스로 제한할 수 없습니다. 이러한 기능은 인스턴스 라이선스에 정의됩니다.
이러한 기능은 GLOBAL_FEATURES에 추가하세요.
EE 기능 가드하기#
라이선스된 기능은 라이선스를 가진 사용자에게만 사용 가능해야 합니다. 사용자가 기능에 접근할 수 있는지 확인하거나 가드하는 검사를 추가해야 합니다.
라이선스된 기능을 가드하려면:
ee/app/models/gitlab_subscriptions/features.rb에서 기능 식별자를 찾으세요.
my_feature_name이 기능 식별자인 경우 다음 메서드를 사용하세요:
프로젝트 컨텍스트에서:
my_project.licensed_feature_available?(:my_feature_name) # true if available for my_project
그룹 또는 사용자 네임스페이스 컨텍스트에서:
my_group.licensed_feature_available?(:my_feature_name) # true if available for my_group
전역(시스템 전체) 기능의 경우:
License.feature_available?(:my_feature_name) # true if available in this instance
선택 사항. 전역 기능이 유료 플랜의 네임스페이스에서도 사용 가능한 경우, 두 기능 식별자를 결합하여 관리자와 그룹 사용자 모두에게 허용할 수 있습니다. 예를 들어:
License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # Both admins and group members can see this EE feature
라이선스가 없을 때 CE 인스턴스 시뮬레이션#
라이선스가 없는 EE 인스턴스에서 GitLab CE 기능이 작동하도록 구현된 이후 GitLab Enterprise Edition은 라이선스가 활성화되지 않은 경우 GitLab Community Edition처럼 작동합니다.
CE 스펙은 가능한 한 변경하지 않아야 하며 EE를 위한 추가 스펙이 추가되어야 합니다. 라이선스된 기능은 EE::LicenseHelpers의 스펙 헬퍼 stub_licensed_features를 사용하여 스텁할 수 있습니다.
ee/ 디렉터리를 삭제하거나 FOSS_ONLY 환경 변수를 true로 평가되는 값으로 설정하여 GitLab이 CE처럼 동작하도록 강제할 수 있습니다. 테스트를 실행할 때도 동일하게 작동합니다
(예: FOSS_ONLY=1 yarn jest).
라이선스된 GDK에서 CE 인스턴스 시뮬레이션#
GDK에서 라이선스를 삭제하지 않고 CE 인스턴스를 시뮬레이션하려면:
gdk.yml에 다음 항목을 추가하세요:
env:
FOSS_ONLY: "1"
그런 다음 GDK를 재시작하세요:
gdk restart
EE 설치로 되돌리려면 gdk.yml에서 환경 변수를 제거하고 2단계를 반복하세요.
CE로 기능 스펙 실행#
기능 스펙을 CE로 실행할 때는 백엔드와 프론트엔드의 에디션이 일치하는지 확인해야 합니다. 이를 위해:
FOSS_ONLY=1 환경 변수를 설정하세요:
export FOSS_ONLY=1
GDK를 시작하세요:
gdk start
기능 스펙을 실행하세요:
bin/rspec spec/features/<path_to_your_spec>
FOSS 컨텍스트에서 CI 파이프라인 실행#
기본적으로 개발을 위한 머지 리퀘스트 파이프라인은 EE 컨텍스트에서만 실행됩니다. FOSS와 EE 간에 다른 기능을 개발하는 경우 FOSS 컨텍스트에서도 파이프라인을 실행하고 싶을 수 있습니다.
두 컨텍스트 모두에서 파이프라인을 실행하려면 머지 리퀘스트에 ~"pipeline:run-as-if-foss" 라벨을 추가하세요.
자세한 내용은 As-if-FOSS job과 크로스 프로젝트 다운스트림 파이프라인 파이프라인 문서를 참조하세요.
백엔드에서의 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 -
ee/config/initializers/foo_bar.rb
이것이 가능한 이유는 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)
EE 백엔드 코드로 CE 기능 확장#
기존 CE 기능을 기반으로 하는 기능의 경우, EE 네임스페이스에 모듈을 작성하고 클래스가 있는 파일의 마지막 줄에서 CE 클래스에 주입하세요. 이렇게 하면 CE 클래스에 한 줄만 추가되기 때문에 CE에서 EE로 병합하는 동안 충돌이 발생할 가능성이 줄어듭니다. 예를 들어, User 클래스에 모듈을 prepend하려면 다음 접근 방식을 사용하세요:
class User < ActiveRecord::Base
# ... lots of code here ...
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합니다.
확장 모듈이 이 네이밍 규약을 따르지 않는 경우, prepend_mod_with, extend_mod_with, 또는 include_mod_with를 사용하여 모듈 이름을 제공할 수도 있습니다. 이 메서드들은 모듈 자체가 아닌 전체 모듈 이름을 포함하는 String을 인수로 받습니다. 예를 들어:
class User
#...
end
User.prepend_mod_with('UserExtension')
모듈은 EE 네임스페이스가 필요하므로 파일도 ee/ 하위 디렉터리에 넣어야 합니다. 예를 들어, EE에서 사용자 모델을 확장하려면 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
CE 기능을 기반으로 하는 EE 기능 테스트#
CE 기능을 EE 기능으로 확장하는 EE 네임스페이스 모듈을 테스트하려면, 두 번째 ee/ 하위 디렉터리를 포함하여 ee/spec 디렉터리에 스펙 파일을 일반적으로 만드세요.
예를 들어, 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를 통해 클래스의 구현에 접근할 수 있습니다.
몇 가지 주의사항이 있습니다:
CE에서 메서드 이름이 변경되었을 때 EE 오버라이드가 자동으로 잊혀지지 않도록 항상 extend ::Gitlab::Utils::Override를 사용하고 override로 overrider 메서드를 가드하세요.
overrider가 CE 구현 중간에 줄을 추가하는 경우, CE 메서드를 리팩토링하고 더 작은 메서드로 분리해야 합니다. 또는 CE에서는 비어 있고 EE 전용 구현이 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
# Follow the above pattern to call super and extend it
end
end
prepend할 때는 ee/ 전용 하위 디렉터리에 배치하고, 네이밍 충돌을 피하기 위해 클래스나 모듈을 module EE로 래핑하세요.
예를 들어, CE 구현인 ApplicationController#after_sign_out_path_for를 오버라이드하려면:
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.in_organization(Organizations::Organization.first).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/initializers의 코드#
Rails 초기화 코드는 다음 위치에 있습니다:
-
CE 전용 기능의 경우
config/initializers -
EE 기능의 경우
ee/config/initializers
분리가 불가능한 경우에만 config/initializers에서 Gitlab.ee { ... }/Gitlab.ee?를 사용하세요. 예를 들어:
SomeGem.configure do |config|
config.base = 'https://example.com'
config.encryption = true if Gitlab.ee?
end
클래스 메서드로 초기화 코드 확장#
초기화 코드에서 사용되는 클래스 메서드를 오버라이드해야 하는 더 복잡한 시나리오에서는 모델과 유사하게 prepend_mod_with 패턴을 사용할 수 있습니다. 이 접근 방식은 app/models를 확장하는 방법을 반영하며 CE와 EE 로직을 깔끔하게 분리할 수 있습니다.
이 패턴은 기능이 모든 EE 인스턴스가 아닌 SaaS 인스턴스에서만 활성화되어야 하는 경우 Gitlab.ee?만으로는 충분하지 않은 초기화 코드에서 SaaS 전용 기능을 구성해야 할 때 특히 유용합니다.
예를 들어, config/initializers/doorkeeper.rb에서:
# The initializer calls a class method that can be overridden in EE
allow_grant_flow_for_client do |grant_flow, client|
next false if Applications::CreateService.disable_ropc_for_all_applications?
# ... other logic
end
CE 서비스(app/services/applications/create_service.rb)에서:
module Applications
class CreateService
# Define class methods that return false by default but can be overridden in EE
def self.disable_ropc_for_all_applications?
false
end
# ... other methods
end
end
# Allow EE to extend this service
Applications::CreateService.prepend_mod_with('Applications::CreateService')
EE 확장(ee/app/services/ee/applications/create_service.rb)에서:
module EE
module Applications
module CreateService
def self.prepended(base)
base.singleton_class.prepend(ClassMethods)
end
module ClassMethods
extend ::Gitlab::Utils::Override
override :disable_ropc_for_all_applications?
def disable_ropc_for_all_applications?
::Gitlab::Saas.feature_available?(:disable_ropc_for_all_applications)
end
end
end
end
end
이 패턴을 통해 초기화 코드는 CE와 EE 사이에서 변경되지 않으면서 에디션에 따라 다른 동작을 하는 메서드를 호출할 수 있습니다.
config/routes의 코드#
config/routes.rb에 draw_all :admin을 추가하면, 애플리케이션은 config/routes/admin.rb에 있는 파일을 로드하고 ee/config/routes/admin.rb에 있는 파일도 로드하려고 시도합니다.
파일을 찾을 수 없으면 오류가 발생합니다.
EE에서는 최소 하나의 파일, 최대 두 개의 파일을 로드해야 합니다. CE에서는 하나의 파일만 로드합니다.
CE와 EE 라우트 파일이 모두 있는 라우트에는 draw_all을 사용하세요.
EE 전용 라우트를 추가하려면 Gitlab.ee와 함께 draw를 사용하세요:
Gitlab.ee do
draw :ee_only
end
app/controllers/의 코드#
컨트롤러에서 가장 일반적인 충돌 유형은 CE에서 액션 목록을 가지고 있는 before_action에서 EE가 해당 목록에 일부 액션을 추가하는 것입니다.
params.require / params.permit 호출에서도 동일한 문제가 자주 발생합니다.
완화 방법
CE와 EE 액션/키워드를 분리하세요. 예를 들어 ProjectsController의 params.require:
def project_params
params.require(:project).permit(project_params_attributes)
end
# Always returns an array of symbols, created however best fits the use case.
# It should be sorted alphabetically.
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
issues_template
merge_requests_template
...
]
end
app/models/의 코드#
EE 전용 모델은 ee/app/models/에 정의되어야 합니다.
CE 모델을 오버라이드하려면 ee/app/models/ee/에 파일을 만들고 prepended 블록에 새 코드를 추가하세요.
ActiveRecord enum은 FOSS에서 완전히 정의되어야 합니다.
app/views/의 코드#
EE가 CE 뷰에 특정 뷰 코드를 추가하는 것은 매우 자주 발생하는 문제입니다. 예를 들어 프로젝트 설정 페이지의 승인 코드가 그렇습니다.
완화 방법
EE 전용 코드 블록은 partial로 이동해야 합니다. 이렇게 하면 들여쓰기를 고려할 때 해결하기 어려운 HAML 코드의 큰 덩어리와의 충돌을 피할 수 있습니다.
EE 전용 뷰는 적절한 하위 디렉터리를 사용하여 ee/app/views/에 배치해야 합니다.
render_if_exists 사용#
일반 render 대신 render_if_exists를 사용해야 합니다. 이 메서드는 특정 partial을 찾을 수 없으면 아무것도 렌더링하지 않습니다. CE와 EE 간에 코드를 동일하게 유지하기 위해 CE에 render_if_exists를 넣을 수 있습니다.
이 방법의 장점:
- CE 코드를 읽을 때 EE 뷰를 확장하는 위치에 대한 매우 명확한 힌트를 제공합니다.
이 방법의 단점:
- partial 이름에 오타가 있으면 자동으로 무시됩니다.
주의 사항#
render_if_exists 뷰 경로 인수는 app/views/ 및 ee/app/views에 상대적이어야 합니다.
CE 뷰 경로에 상대적인 EE 템플릿 경로를 해석하는 것은 작동하지 않습니다.
- # app/views/projects/index.html.haml
= render_if_exists 'button' # Will not render `ee/app/views/projects/_button` and will quietly fail
= render_if_exists 'projects/button' # Will render `ee/app/views/projects/_button`
render_ce 사용#
render와 render_if_exists의 경우, EE partial을 먼저 찾고 CE partial을 찾습니다. 이름이 같은 모든 partial이 아닌 특정 partial만 렌더링합니다. 이 점을 활용하면 동일한 partial 경로(예: projects/settings/archive)가 CE에서는 CE partial(app/views/projects/settings/_archive.html.haml), EE에서는 EE partial(ee/app/views/projects/settings/_archive.html.haml)을 참조할 수 있습니다. 이렇게 하면 CE와 EE 간에 서로 다른 것을 표시할 수 있습니다.
그러나 때로는 EE partial에서 CE partial을 재사용하고 싶을 수도 있습니다. 기존 CE partial에 무언가를 추가하고 싶기 때문입니다. 다른 이름으로 다른 partial을 추가하여 이 문제를 해결할 수 있지만, 그렇게 하면 번거로울 수 있습니다.
이 경우 EE partial을 무시하는 render_ce를 사용할 수 있습니다. 한 가지 예시는
ee/app/views/projects/settings/_archive.html.haml입니다:
- return if @project.self_deletion_scheduled?
= render_ce 'projects/settings/archive'
위 예시에서 같은 EE partial을 찾아 무한 재귀를 일으키기 때문에
render 'projects/settings/archive'를 사용할 수 없습니다. 대신 render_ce를 사용하면 ee/의 partial을 무시하고 동일한 경로(projects/settings/archive)의 CE partial(app/views/projects/settings/_archive.html.haml)을 렌더링합니다. 이렇게 하면 CE partial을 쉽게 래핑할 수 있습니다.
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?
::Database::BatchedBackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, self.class.name.demodulize, table_name) if table_name
end
end
end
end
end
app/graphql/의 코드#
EE 전용 mutation, resolver, 타입은
ee/app/graphql/{mutations,resolvers,types}에 추가해야 합니다.
CE mutation, resolver, 타입을 오버라이드하려면 ee/app/graphql/ee/{mutations,resolvers,types}에 파일을 만들고 prepended 블록에 새 코드를 추가하세요.
예를 들어, CE에 Mutations::Tanukis::Create라는 mutation이 있고 새 인수를 추가하고 싶다면 ee/app/graphql/ee/mutations/tanukis/create.rb에 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/의 코드#
CE를 오버라이드하는 EE 로직은 최상위 EE 모듈 네임스페이스에 배치하세요. 일반적으로 하는 것처럼 EE 모듈 아래에 클래스를 네임스페이스로 지정하세요.
예를 들어, CE에 lib/gitlab/ldap/에 LDAP 클래스가 있다면 EE 전용 오버라이드는 ee/lib/ee/gitlab/ldap에 배치하세요.
CE에 대응하지 않는 EE 전용 클래스는 ee/lib/gitlab/ldap에 배치합니다.
lib/api/의 코드#
prepend_mod_with 한 줄로 EE 기능을 확장하는 것은 매우 까다로울 수 있으며, 각각의 다른 Grape 기능에 대해 다른 전략이 필요할 수 있습니다. 다양한 전략을 쉽게 적용하기 위해 EE 모듈에서 extend ActiveSupport::Concern을 사용합니다.
EE 모듈 파일은 EE 백엔드 코드로 CE 기능 확장에 따라 배치하세요.
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 파라미터#
EE에서 정의된 파라미터를 포함하기 위해 params를 정의하고 다른 params 정의에서 use를 사용할 수 있습니다. 그러나 EE가 오버라이드할 수 있도록 먼저 CE에서 "인터페이스"를 정의해야 합니다. prepend_mod_with로 인해 다른 곳에서는 이렇게 할 필요가 없지만, Grape는 내부적으로 복잡하여 그렇게 하기 어려우므로, 여기서 먼저 인터페이스를 정의하는 일반적인 객체지향 관행을 따릅니다.
예를 들어, EE에 몇 가지 선택적 파라미터가 더 있다고 가정합니다. 클래스에서 사용되기 전에 주입할 수 있도록 Grape::API::Instance 클래스에서 파라미터를 헬퍼 모듈로 이동할 수 있습니다.
module API
class Projects < Grape::API::Instance
helpers Helpers::ProjectsHelpers
end
end
다음과 같은 CE API params가 있다면:
module API
module Helpers
module ProjectsHelpers
extend ActiveSupport::Concern
extend Grape::API::Helpers
params :optional_project_params_ce do
# CE specific params go here...
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 specific params go here...
end
end
end
end
end
end
EE 헬퍼#
EE 모듈이 CE 헬퍼를 쉽게 오버라이드할 수 있도록 먼저 확장하려는 헬퍼를 정의해야 합니다. 쉽고 명확하게 하기 위해 클래스 정의 직후에 하도록 하세요:
module API
module Ci
class JobArtifacts < Grape::API::Instance
# EE::API::Ci::JobArtifacts would override the following helpers
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 전용 동작이 필요합니다. 일반적으로 EE 메서드를 사용하여 CE 메서드를 오버라이드할 수 있지만, API 라우트는 메서드가 아니므로 오버라이드할 수 없습니다. 독립적인 메서드로 추출하거나 CE 라우트에서 동작을 주입할 수 있는 "훅"을 도입해야 합니다. 예를 들어:
module API
class MergeRequests < Grape::API::Instance
helpers do
# EE::API::MergeRequests would override the following helpers
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 모듈에서 확장하는 것은 매우 어려우며, 이것은 특정 라우트에 대한 메타데이터를 저장합니다. 따라서 EE의 route_setting을 CE에 두어도 괜찮습니다. CE에서 해당 메타데이터를 사용하지 않기 때문입니다.
route_setting을 더 많이 사용하게 되고 EE에서 확장이 정말 필요한지에 따라 이 정책을 재검토할 수 있습니다. 지금은 많이 사용하지 않습니다.
EE 전용 데이터 설정을 위한 클래스 메서드 활용#
때로는 특정 API 라우트에 다른 인수를 사용해야 하는데, Grape가 다른 블록에서 다른 컨텍스트를 가지기 때문에 EE 모듈로 쉽게 확장할 수 없습니다. 이를 극복하기 위해 별도의 모듈이나 클래스에 있는 클래스 메서드로 데이터를 이동해야 합니다. 이를 통해 데이터가 사용되기 전에 해당 모듈이나 클래스를 확장할 수 있으며, CE 코드 중간에 prepend_mod_with를 배치할 필요가 없습니다.
예를 들어, 한 곳에서 API가 EE 전용 인수를 최소 인수로 고려할 수 있도록 at_least_one_of에 추가 인수를 전달해야 합니다. 다음과 같이 접근합니다:
# 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/spec 폴더에 EE 스펙을 배치하세요.
기본적으로 CE 스펙은 EE 코드가 로드된 상태에서 실행됩니다. EE가 라이선스 없이 실행될 때 그대로 작동해야 합니다.
이 스펙들은 EE 코드가 제거되었을 때도 통과해야 합니다. CE 인스턴스를 시뮬레이션하여 EE 코드 없이 테스트를 실행할 수 있습니다.
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가 있을 수 있습니다.
대응하는 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
// (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';
프론트엔드에서 새 EE 전용 기능 추가#
개발 중인 기능이 CE에 없는 경우 ee/에 엔트리 포인트를 추가하세요. 예를 들어:
# Add HTML element to mount
ee/app/views/admin/geo/designs/index.html.haml
# Init the application
ee/app/assets/javascripts/pages/ee_only_feature/index.js
# Mount the feature
ee/app/assets/javascripts/ee_only_feature/index.js
licensed_feature_available? 및 License.feature_available?로 기능을 가드하는 것은 일반적으로 백엔드 가이드에 설명된 대로 컨트롤러에서 발생합니다.
EE 전용 프론트엔드 기능 테스트#
CE와 동일한 디렉터리 구조를 따라 ee/spec/frontend/에 EE 테스트를 추가하세요.
라이선스 기능 활성화에 대한 참고 사항은 EE 전용 백엔드 기능 테스트를 확인하세요.
EE 프론트엔드 코드로 CE 기능 확장#
기존 뷰를 확장하는 프론트엔드 기능을 가드하려면 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에 기능이 나타나는지 확인하세요.
EE Vue 컴포넌트로 Vue 애플리케이션 확장#
UI의 기존 기능을 향상시키는 EE 라이선스 기능은 Vue 애플리케이션에 새 요소나 인터랙션을 컴포넌트로 추가합니다.
EE 기능을 추가하기 위해 CE 컴포넌트 내에서 EE 컴포넌트를 가져올 수 있습니다.
ee_component 별칭을 사용하여 EE 컴포넌트를 가져오세요. EE에서 ee_component 가져오기 별칭은 ee/app/assets/javascripts 디렉터리를 가리킵니다. CE에서 이 별칭은 아무것도 렌더링하지 않는 빈 컴포넌트로 해석됩니다.
다음은 CE 컴포넌트에 가져온 EE 컴포넌트의 예시입니다:
<script>
// app/assets/javascripts/feature/components/form.vue
// In EE this will be resolved as `ee/app/assets/javascripts/feature/components/my_ee_component.vue`
// In CE as `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>
CE 코드베이스 내에서 렌더링이 일부 검사(예: 기능 플래그 검사)에 의존하는 경우 EE 컴포넌트를 [비동기적으로](https://v2.vuejs.org/v2/guide/components-dynamic-async.html#Async-Components) 가져올 수 있습니다.
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() {
// Comes from gon.licensed_features as a camel-case version of `my_feature_name`
return this.glFeatures.myFeatureName;
}
},
};
</script>
<template>
<div v-if="shouldRenderComponent">
<!-- EE licensed feature UI -->
</div>
</template>
mixin은 절대적으로 필요한 경우가 아니면 사용하지 마세요. 대안적인 패턴을 찾으세요.
권장 대안 접근 방식 (이름 있는/범위 있는 슬롯)#
-
mixin으로 했던 것과 동일한 작업을 달성하기 위해 슬롯 및/또는 범위 있는 슬롯을 사용할 수 있습니다. EE 컴포넌트만 필요한 경우 CE 컴포넌트를 만들 필요가 없습니다.
-
먼저, CE 기본 위에 EE 템플릿과 기능을 데코레이트해야 하는 경우 슬롯을 렌더링할 수 있는 CE 컴포넌트가 있습니다.
// ./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
<slot name="ee-specific-component">
</template>
- 다음으로, EE 컴포넌트를 렌더링하고, EE 컴포넌트 내에서 CE 컴포넌트를 렌더링하고 슬롯에 추가 콘텐츠를 추가합니다.
// ./ee/my_component.vue
<script>
export default {
computed: {
tooltipText() {
if (this.weight) {
return "5 issues with weight 10";
}
}
},
methods: {
submit() {
// do something.
}
},
}
</script>
<template>
<my-component :tooltipDefaultText="tooltipText">
<template #ee-specific-component>
EE Specific Value
<button @click="submit">Click Me</button>
</template>
</my-component>
</template>
- 마지막으로, 컴포넌트가 필요한 곳에서 다음과 같이 가져올 수 있습니다.
import MyComponent from 'ee_else_ce/path/my_component'.vue
- 이 방법으로 CE 또는 EE 구현에 적합한 컴포넌트가 포함됩니다.
같은 computed 값에 대해 다른 결과가 필요한 EE 컴포넌트의 경우, 위 예시와 같이 CE 래퍼에 props를 전달할 수 있습니다.
- EE 추가 HTML
EE에서 추가 HTML이 있는 템플릿의 경우 새 컴포넌트로 이동하고 ee_else_ce 가져오기 별칭을 사용해야 합니다.
다른 JS 코드 확장#
JS 파일을 확장하려면 다음 단계를 완료하세요:
- EE 전용 코드가
ee/폴더 내에 있어야 하는 경우ee_else_ce헬퍼를 사용하세요.
EE 전용 코드만 있는 EE 파일을 만들고 CE 대응 파일을 확장하세요.
- 확장할 수 없는 함수 내부의 코드의 경우, 코드를 새 파일로 이동하고
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 = () => {
// ...
};
// ... other CE functions ...
// ee/app/assets/javascripts/feature/utils.js
import {
myFunction as ceMyFunction,
} from '~/feature/utils';
/* eslint-disable import/export */
// Export same utils as CE
export * from '~/feature/utils';
// Only override `myFunction`
export const myFunction = () => {
const result = ceMyFunction();
// add EE feature logic
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
// ...
// because we referenced the component using ee_else_ce we have to do the same in the spec.
import Friend from 'ee_else_ce/components/friend.vue;'
describe('ComponentUnderTest', () => {
const findFriend = () => wrapper.find(Friend);
it('renders friend', () => {
// This would fail in CE if we did `ee/component...`
// and would fail in EE if we did `~/component...`
expect(findFriend().exists()).toBe(true);
});
});
EE vs CE 테스트 실행#
CE와 EE 환경 모두를 위한 테스트를 만들 때마다 두 테스트가 로컬과 파이프라인에서 실행될 때 통과하는지 확인하기 위한 몇 가지 단계를 거쳐야 합니다.
-
기본적으로 테스트는 EE 환경에서 실행되어 EE와 CE 테스트를 모두 실행합니다.
-
FOSS 환경에서 CE 파일만 테스트하려면 다음 명령을 실행해야 합니다:
FOSS_ONLY=1 yarn jest path/to/spec/file.spec.js
CE 테스트에는 CE 기능만 추가하므로, EE 전용 mock 데이터가 없으면 EE 환경에서 실패할 수 있습니다. CE 테스트가 두 환경 모두에서 작동하도록 하려면:
- mock 데이터를 가져올 때
ee_else_ce_jest별칭을 사용하세요. 예를 들어:
import { sidebarDataCountResponse } from 'ee_else_ce_jest/super_sidebar/mock_data';
-
CE 파일에 CE 기능 데이터만 있는 CE
mock_data파일과 CE 및 EE 기능 데이터가 모두 있는 EE 파일이 있는지 확인하세요(sidebarDataCountResponse와 같은 객체 포함). -
CE 파일
expect블록에서 객체를 비교해야 하는 경우toEqual대신toMatchObject를 사용하세요. 그러면 CE 데이터에 EE 데이터가 존재할 것을 기대하지 않습니다. 예를 들어:
expect(findPinnedSection().props('asyncCount')).toMatchObject(asyncCountData);
assets/stylesheets의 SCSS 코드#
추가하는 컴포넌트의 스타일이 EE로 제한된 경우, app/assets/stylesheets 내 적절한 디렉터리에 별도의 SCSS 파일을 두는 것이 좋습니다.
경우에 따라 이것이 완전히 가능하지 않거나 전용 SCSS 파일을 만드는 것이 과도한 경우도 있습니다. 예를 들어, 일부 컴포넌트의 텍스트 스타일이 EE에서 다른 경우입니다. 이러한 경우 스타일은 보통 CE와 EE 모두에 공통적인 스타일시트에 유지되며, CE에서 EE로 병합하는 동안 충돌을 피하기 위해 나머지 CE 규칙에서 해당 규칙 세트를 격리하는 것이 좋습니다(동일한 내용을 설명하는 주석 추가와 함께).
// Bad
.section-body {
.section-title {
background: $gl-header-color;
}
&.ee-section-body {
.section-title {
background: $gl-header-color-cyan;
}
}
}
// Good
.section-body {
.section-title {
background: $gl-header-color;
}
}
// EE-specific start
.section-body.ee-section-body {
.section-title {
background: $gl-header-color-cyan;
}
}
// EE-specific end
GitLab-svgs#
app/assets/images/icons.json 또는 app/assets/images/icons.svg의 충돌은
yarn run svg로 해당 에셋을 재생성하여 해결할 수 있습니다.