---
stage: none
group: unassigned
info: 拥有至少Maintainer角色的用户可以合并此内容的更新。详情请参阅 https://docs.gitlab.com/development/development_processes/#development-guidelines-review。
title: 企业版功能实施指南
---
- **将代码放在`ee/`目录下**:将所有企业版(EE)代码放入顶级`ee/`目录。其余代码必须尽可能接近社区版(CE)文件。
- **编写测试**:与任何代码一样,EE功能必须有良好的测试覆盖率以防止回归。所有`ee/`代码必须在`ee/`中有对应的测试。
- **编写文档**:在`doc/`目录中添加文档。描述该功能并包含截图(如果适用)。注明[适用的版本](documentation/styleguide/availability_details.md)。
<!-- markdownlint-disable MD044 -->
- **向[`www-gitlab-com`](https://gitlab.com/gitlab-com/www-gitlab-com)项目提交合并请求(MR)**:将该新功能添加到[EE功能列表](https://about.gitlab.com/features/)中。
<!-- markdownlint-enable MD044 -->
## 开发中的运行时模式
1. **未授权的企业版**:这是从普通GDK安装中获得的状态,如果你从[主仓库](https://gitlab.com/gitlab-org/gitlab)安装
1. **已授权的企业版**:当你[向GDK添加有效许可证](https://gitlab.com/gitlab-org/customers-gitlab-com/-/blob/main/doc/setup/gitlab.md#adding-a-license-from-staging-customers-portal-to-your-gdk)时
1. **GitLab.com SaaS**:当你[模拟SaaS实例](#simulate-a-saas-instance)时
1. **社区版**:在上述任何状态下,当你[模拟社区版实例](#simulate-a-ce-instance-with-a-licensed-gdk)时
## 仅适用于SaaS的功能
开发仅适用于SaaS的功能(例如CustomersDot集成)时,请遵循以下指南。
一般来说,功能应同时提供给[ both SaaS and self-managed deployments ](https://handbook.gitlab.com/handbook/product/product-principles/#parity-between-saas-and-self-managed-deployments)。然而,有些情况下功能应仅在SaaS上可用,本指南将帮助说明如何实现这一点。
建议使用`Gitlab::Saas.feature_available?`。这能围绕功能仅为SaaS的原因提供丰富的上下文定义。
### 使用`Gitlab::Saas.feature_available?`实现仅SaaS功能
#### 添加到FEATURES常量中
1. 参见[命名空间概念指南](software_design.md#use-namespaces-to-define-bounded-contexts),帮助为新SaaS功能命名。
1. 在`ee/lib/ee/gitlab/saas.rb`中将新功能添加到`FEATURE`中:
```ruby
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代码库提供了[`bin/saas-feature.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/bin/saas-feature.rb),这是一个专用工具,用于创建新的SaaS功能定义。
该工具会询问有关新SaaS功能的各类问题,随后在`ee/config/saas_features`中生成YAML定义文件。
只有具备YAML定义文件的SaaS功能,才可在开发或测试环境中使用。
```shell
❯ bin/saas-feature my_saas_feature
您选择了组 'group::acquisition'
>> 引入SaaS功能的MR URL(按回车键跳过,让Danger直接在MR中提供建议):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
创建 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实例(极狐)上选择退出仅限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助手
若要在测试中启用仅限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元数据助手
依据测试类型不同,stub_saas_features方法可能无法满足SaaS场景需求。
此时可使用:saas RSpec元数据助手。
关于测试的更多细节,请参考 依赖SaaS的测试。
借助元数据助手测试双分支逻辑示例如下:
it '显示自定义项目模板选项卡' do
page.within '.project-template .custom-instance-project-templates-tab' do
expect(page).to have_content 'Instance'
end
end
context '当SaaS时', :saas do
it '不显示Instance选项卡' do
page.within '.project-template' do
expect(page).not_to have_content 'Instance'
end
end
end模拟SaaS实例
若您在本地开发,需让实例模拟产品(GitLab.com)版本的SaaS:
-
导出以下环境变量:
export GITLAB_SIMULATE_SAAS=1向本地GitLab实例传递环境变量的方式有多种。 例如,您可在GDK根目录下创建
env.runit文件,写入上述片段。 -
启用允许使用授权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。
这里有📺 视频演示上述步骤。
实现新的企业版功能
如果你正在开发 GitLab Premium 或 GitLab Ultimate 许可的功能,使用以下步骤添加新功能或扩展它。
GitLab 许可功能被添加到 ee/app/models/gitlab_subscriptions/features.rb。要确定如何修改此文件,首先与你的产品经理讨论你的功能如何融入我们的许可体系。
使用以下问题指导你:
- 这是新功能,还是正在扩展现有许可功能?
- 如果你的功能已存在,则不必修改
features.rb,但必须找到现有功能标识符以保护它。 - 如果这是新功能,决定一个标识符(如
my_feature_name),将其添加到features.rb文件中。
- 如果你的功能已存在,则不必修改
- 这是 GitLab Premium 还是 GitLab Ultimate 功能?
- 根据你选择使用的计划,将功能标识符添加到
PREMIUM_FEATURES或ULTIMATE_FEATURES中。
- 根据你选择使用的计划,将功能标识符添加到
- 此功能是否会全局可用(整个 GitLab 实例系统级)?
保护你的企业版功能
许可功能仅对许可用户可用。你必须添加检查或保护逻辑来确定用户是否有权访问该功能。
要保护你的许可功能:
-
在
ee/app/models/gitlab_subscriptions/features.rb中找到你的功能标识符。 -
使用以下方法(其中
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
-
-
可选。如果你的全局功能也对付费计划的命名空间可用,结合两个功能标识符以允许管理员和组成员都能看到此企业版功能。例如:
License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # 管理员和组成员都可以看到此 EE 功能
未授权时模拟社区版实例
在实现了 让 GitLab 社区版功能在无许可的企业版实例上工作 后,当没有活跃许可证时,GitLab 企业版的行为就像 GitLab 社区版一样。
社区版规范应尽可能保持不变,并为企业版添加额外规范。可以使用 EE::LicenseHelpers 中的规范助手 stub_licensed_features 来存根许可功能。
你可以通过删除 ee/ 目录或将 FOSS_ONLY 环境变量 设置为求值为 true 的值来强制 GitLab 表现为社区版。运行测试时也是如此(例如 FOSS_ONLY=1 yarn jest)。
使用许可的 GDK 模拟社区版实例
要在不删除许可证的 GDK 中模拟社区版:
-
在 GDK 根目录创建一个
env.runit文件,包含以下行:export FOSS_ONLY=1 -
然后重启 GDK:
gdk restart rails && gdk restart webpack
如果想要恢复为企业版安装,移除 env.runit 中的行并重复第 2 步。
作为社区版运行功能规范
运行 功能规范 时,应确保前后端版本匹配。为此:
-
设置
FOSS_ONLY=1环境变量:export FOSS_ONLY=1 -
启动 GDK:
gdk start -
运行功能规范:
bin/rspec spec/features/<your_spec_path>
在开源背景下运行 CI 管道
默认情况下,用于开发的合并请求管道仅在 EE 上下文中运行。如果你正在开发 FOSS 与 EE 有差异的功能,你可能希望也在 FOSS 上下文中运行管道。
若要在两种上下文中运行管道,向合并请求添加 ~"pipeline:run-as-if-foss" 标签。
有关更多信息,请参阅 As-if-FOSS 作业和跨项目下游管道 管道文档。
后端的企业版代码分离
仅企业版(EE)功能
如果正在开发的功能在任何形式的CE中都不存在,我们不需要将代码放在EE命名空间下。例如,一个EE模型可以放入:ee/app/models/awesome.rb,使用Awesome作为类名。这不仅适用于模型。以下是其他示例:
ee/app/controllers/foos_controller.rbee/app/finders/foos_finder.rbee/app/helpers/foos_helper.rbee/app/mailers/foos_mailer.rbee/app/models/foo.rbee/app/policies/foo_policy.rbee/app/serializers/foo_entity.rbee/app/serializers/foo_serializer.rbee/app/services/foo/create_service.rbee/app/validators/foo_attr_validator.rbee/app/workers/foo_worker.rbee/app/views/foo.html.hamlee/app/views/foo/_bar.html.hamlee/config/initializers/foo_bar.rb
之所以这样是因为,对于CE中eager-load/auto-load路径下的每条路径,我们在config/application.rb中添加了相同的以ee/开头的路径。这也适用于视图。
测试仅企业版的(EE)后端功能
要测试在CE中不存在的EE类,像往常一样在ee/spec目录中创建spec文件,但不包含第二个ee/子目录。例如,类ee/app/models/vulnerability.rb的测试位于ee/spec/models/vulnerability_spec.rb。
默认情况下,许可功能在specs/目录中的spec中被禁用。ee/spec目录中的spec默认已初始化Starter许可证。
为了有效测试您的功能,您必须使用stub_licensed_features辅助方法显式启用该功能,例如:
stub_licensed_features(my_awesome_feature_name: true)使用EE后端代码扩展CE功能
对于构建于现有CE功能之上的功能,请在EE命名空间中编写一个模块,并将其注入到CE类中,注入位置在该类所在的文件的最后一行。这使得在CE到EE的合并过程中发生冲突的可能性降低,因为仅在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/子目录中。例如,我们要在EE中扩展用户模型,所以我们有一个名为::EE::User的模块,放在ee/app/models/ee/user.rb中。
这不仅适用于模型。以下是其他示例:
ee/app/controllers/ee/foos_controller.rbee/app/finders/ee/foos_finder.rbee/app/helpers/ee/foos_helper.rbee/app/mailers/ee/foos_mailer.rbee/app/models/ee/foo.rbee/app/policies/ee/foo_policy.rbee/app/serializers/ee/foo_entity.rbee/app/serializers/ee/foo_serializer.rbee/app/services/ee/foo/create_service.rbee/app/validators/ee/foo_attr_validator.rbee/app/workers/ee/foo_worker.rb
测试基于CE功能的EE功能
要测试扩展CE类以添加EE功能的EE命名空间模块,像往常一样在ee/spec目录中创建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中为空但在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包裹类或模块以避免命名冲突。
例如,要覆盖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.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初始化代码位于:
config/initializers用于CE-only功能ee/config/initializers用于EE功能
仅在无法分割时才在config/initializers中使用Gitlab.ee { ... }/Gitlab.ee?。例如:
SomeGem.configure do |config|
config.base = 'https://example.com'
config.encryption = true if Gitlab.ee?
end配置文件 config/routes 中的代码
当我们在 config/routes.rb 中添加 draw :admin 时,应用会尝试加载位于 config/routes/admin.rb 的文件,同时也尝试加载位于 ee/config/routes/admin.rb 的文件。
在 EE(企业版)中,应该至少加载一个文件,最多两个文件。如果找不到任何文件,就会抛出错误。在 CE(社区版)中,由于我们不知道是否存在 EE 路由,即使找不到任何东西也不会抛出错误。
这意味着如果我们想扩展某个特定的 CE 路由文件,只需在 ee/config/routes 中添加同名文件即可。如果我们想添加仅限 EE 的路由,仍然可以在 CE 和 EE 中都添加 draw :ee_only,并在 EE 中添加 ee/config/routes/ee_only.rb,类似于 render_if_exists 的做法。
控制器 app/controllers/ 中的代码
在控制器中,最常见的冲突类型是 before_action,它在 CE 中有一系列操作,但 EE 会向该列表添加一些操作。
同样的问题经常出现在 params.require / params.permit 调用中。
缓解措施
分离 CE 和 EE 的操作/关键字。例如在 ProjectsController 中的 params.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 枚举应完全 在 FOSS 中定义。
视图 app/views/ 中的代码
一个非常常见的问题是 EE 在 CE 视图中添加了一些特定的视图代码。例如项目设置页面中的审批代码。
缓解措施
EE 特定的代码块应移动到局部模板(partials)中。这可以避免与大段 HAML 代码发生冲突,而这些代码在考虑缩进时很难解决。
EE 特定的视图应放置在 ee/app/views/ 中,如果合适可以使用额外的子目录。
使用 render_if_exists
我们不应使用常规的 render,而是使用 render_if_exists,如果找不到特定的局部模板,则不会渲染任何内容。我们这样做是为了可以在 CE 中使用 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
对于 render 和 render_if_exists,它们会先搜索 EE 局部模板,然后再搜索 CE 局部模板。它们只会渲染特定的局部模板,而不是所有同名局部模板。我们可以利用这一点,使得相同的局部模板路径(例如 projects/settings/archive)可以指向 CE 中的 CE 局部模板(即 app/views/projects/settings/_archive.html.haml),而在 EE 中则是 EE 局部模板(即 ee/app/views/projects/settings/_archive.html.haml)。这样,我们就可以在 CE 和 EE 之间显示不同的内容。
然而,有时我们也希望在 EE 局部模板中重用 CE 局部模板,因为我们可能只是想在现有的 CE 局部模板中添加一些内容。我们可以通过添加另一个不同名称的局部模板来解决这个问题,但这会很繁琐。
在这种情况下,我们可以使用 render_ce,它会忽略任何 EE 局部模板。示例如下(ee/app/views/projects/settings/_archive.html.haml):
- return if @project.self_deletion_scheduled?
= render_ce 'projects/settings/archive'在上面的示例中,我们不能使用 render 'projects/settings/archive',因为这会找到同一个 EE 局部模板,导致无限递归。相反,我们可以使用 render_ce,它会忽略 ee/ 中的任何局部模板,然后渲染相同路径(即 projects/settings/archive)下的 CE 局部模板(即 app/views/projects/settings/_archive.html.haml)。这样我们就可以轻松地包装 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
endapp/graphql/ 中的代码
EE 特定的 mutation、resolver 和 type 应该添加到 ee/app/graphql/{mutations,resolvers,types} 中。
若要覆盖 CE 的 mutation、resolver 或 type,请在 ee/app/graphql/ee/{mutations,resolvers,types} 中创建文件,并将新代码添加到 prepended 块中。
例如,如果 CE 有一个名为 Mutations::Tanukis::Create 的 mutation,而你想要添加一个新的参数,则将 EE 覆盖代码放在 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 名称'
end
end
end
end
endlib/ 中的代码
将覆盖 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: '项目的 ID 或 URL 编码路径'
end
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
# ...
end
end
end
end
end由于命名空间的不同,我们需要对某些常量使用完整限定符。
企业版参数
我们可以定义 params 并在另一个 params 定义中使用 use 来包含企业版(EE)中定义的参数。但是,我们需要先在社区版(CE)中定义“接口”,以便企业版(EE)可以覆盖它。由于 prepend_mod_with,我们在其他地方不需要这样做,但Grape内部很复杂,我们无法轻易做到这一点,因此我们在这里遵循常规的面向对象实践来定义接口。
例如,假设我们有更多可选的企业版参数。我们可以将这些参数从 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
# 社区版特定的参数放在这里...
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
# 企业版特定的参数放在这里...
end
end
end
end
end
end企业版帮助方法
为了方便企业版(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企业版特定行为
有时我们需要在某些API中加入企业版(EE)特有的行为。通常我们可以使用企业版的方法覆盖社区版的方法,然而API路由不是方法,因此无法被覆盖。我们需要将它们提取成一个独立的方法,或者在社区版的路线中引入一些“钩子”,让我们可以在那里注入行为。类似这样:
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 在社区版中不做任何事情,但我们可以在企业版中覆盖它:
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企业版 route_setting
这很难在企业版(EE)模块中扩展,而且这是为特定路由存储元数据的。鉴于此,我们可以将企业版的 route_setting 留在社区版中,因为它不会造成伤害,并且我们在社区版中不使用这些元数据。
当我们更多地使用 route_setting 并且确实需要从企业版中扩展它时,我们可以重新审视这一策略。目前我们使用不多。
利用类方法设置企业版特定数据
有时我们需要为一个特定的API路由使用不同的参数,但由于Grape在不同块中有不同的上下文,我们无法轻松地通过企业版模块来扩展它。为了克服这个问题,我们需要将这些数据移动到一个单独的模块或类中的类方法中。这使得我们可以在其数据被使用之前扩展该模块或类,而不必在社区版代码中间放置一个 prepend_mod_with。
例如,在一个地方,我们需要向 at_least_one_of 传递一个额外的参数,以便API可以将仅限企业版的参数视为最少的参数。我们会采用如下方式:
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类方法中扩展该参数:
```ruby
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规范放在 ee/spec 文件夹中。
默认情况下,CE规范会加载EE代码运行,因为当EE在没有许可证的情况下运行时,它们应保持原样工作。
当移除EE代码时,这些规范也需要通过。你可以通过模拟CE实例在不加载EE代码的情况下运行测试。
在 spec/factories 中的代码
使用 FactoryBot.modify 来扩展CE中已定义的工厂。
你不能在 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
// (仅适用于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';在前端添加新的仅限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
\n\n如果 EE 组件在其呈现依赖于某些检查(例如功能标志检查)的情况下渲染 CE 代码库,则可以异步导入该组件。\n\n\n\n对 `licensed_feature_available?` 和 `License.feature_available?` 进行特性保护通常发生在控制器中,如 [后端指南](#ee-only-features) 中所述。\n\n#### 测试仅 EE 的前端功能\n\n将您的 EE 测试添加到 `ee/spec/frontend/`,遵循您用于 CE 的相同目录结构。\n\n查看 [测试仅 EE 的后端功能](#testing-ee-only-backend-features) 下方的说明,了解启用许可功能的相关内容。\n\n### 使用 EE 前端代码扩展 CE 功能\n\n使用 [`push_licensed_feature`](#guard-your-ee-feature) 来保护扩展现有视图的前端功能:\n\nruby\n\n# ee/app/controllers/ee/admin/my_controller.rb\nbefore_action do\n push_licensed_feature(:my_feature_name) # 用于全局功能\nend\n\n\nruby\n\n# ee/app/controllers/ee/group/my_controller.rb\nbefore_action do\n push_licensed_feature(:my_feature_name, @group) # 用于群组页面\nend\n\n\nruby\n\n# ee/app/controllers/ee/project/my_controller.rb\nbefore_action do\n push_licensed_feature(:my_feature_name, @group) # 用于群组页面\n push_licensed_feature(:my_feature_name, @project) # 用于项目页面\nend\n\n\n在浏览器控制台中验证您的功能是否出现在 `gon.licensed_features` 中。\n\n#### 使用 EE Vue 组件扩展 Vue 应用程序\n\n增强现有 UI 功能的 EE 许可功能会将新元素或交互作为组件添加到您的 Vue 应用程序中。\n\n您可以在 CE 组件内导入 EE 组件以添加 EE 功能。\n\n使用 `ee_component` 别名来导入 EE 组件。在 EE 中,`ee_component` 导入别名指向 `ee/app/assets/javascripts` 目录。而在 CE 中,此别名将被解析为一个不渲染任何内容的空组件。\n\n以下是将 EE 组件导入到 CE 组件中的示例:\n\nvue\n\n\n\n \n\nvue\n\n\n\n
\n\n除非绝对必要,否则不要使用 mixin。尝试寻找替代模式。\n\n
推荐替代方法(命名/作用域插槽)
- 我们可以使用插槽和/或作用域插槽来实现与混入相同的效果。如果只需要 EE 组件,则无需创建 CE 组件。
- 首先,我们有一个 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>- 接下来,我们渲染 EE 组件,并在 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 特定值</span>
<button @click="submit">点击我</button>
</template>
</my-component>
</template>- 最后,在任何需要组件的地方,我们可以像这样引入它:
import MyComponent from 'ee_else_ce/path/my_component'.vue
- 这样无论 CE 还是 EE 实现,都会包含正确的组件
对于需要相同计算值产生不同结果的 EE 组件,我们可以向 CE 包装器传递 props,如示例所示。
- EE 额外 HTML
- 对于 EE 中包含额外 HTML 的模板,我们应该将其移至新组件并使用
ee_else_ce导入别名
- 对于 EE 中包含额外 HTML 的模板,我们应该将其移至新组件并使用
扩展其他 JS 代码
要扩展 JS 文件,完成以下步骤:
- 使用
ee_else_ce助手,其中仅 EE 的代码必须位于ee/文件夹内。- 创建一个仅包含 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 = () => {
// ...
};
// ... 其他 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_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);
});
});运行 EE 与 CE 测试
当你为 CE 和 EE 环境创建测试时,需要采取一些步骤确保本地和流水线运行时两者都能通过。
- 默认情况下,测试在 EE 环境中运行,同时执行 EE 和 CE 测试。
- 如果你想在 FOSS 环境中仅测试 CE 文件,需运行以下命令:
FOSS_ONLY=1 yarn jest path/to/spec/file.spec.js对于 CE 测试,我们只添加 CE 功能,若缺少 EE 特定的模拟数据,可能在 EE 环境中失败。为确保 CE 测试在两种环境中都正常工作:
- 导入模拟数据时使用
ee_else_ce_jest别名。例如:
import { sidebarDataCountResponse } from 'ee_else_ce_jest/super_sidebar/mock_data';-
确保 CE 和 EE 的
mock_data文件中都包含对应数据的对象(如上例中的sidebarDataCountResponse)。一个文件仅包含 CE 功能的数据(用于 CE 文件),另一个则包含 CE 和 EE 功能的数据。 -
在 CE 文件的
expect块中,若需比较对象,请使用toMatchObject而非toEqual,避免期望 CE 数据中存在 EE 数据。例如:
expect(findPinnedSection().props('asyncCount')).toMatchObject(asyncCountData);assets/stylesheets 中的 SCSS 代码
如果你正在添加样式的组件仅限 EE,最好在 app/assets/stylesheets 内的合适目录下有单独的 SCSS 文件。
在某些情况下,这并非完全可行或创建专用 SCSS 文件过于繁琐(例如某组件的文本样式在 EE 中不同)。此时,样式通常保存在 CE 和 EE 共用的样式表中,明智的做法是将这类规则集与其他 CE 规则隔离(同时添加注释说明),以避免 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 重新生成这些资源来解决。