- SaaSのみの機能
- 新しいEE機能の実装
- バックエンドのEEコードの分離
- フロントエンドでのEEコードの分離
Enterprise Editionの機能を実装するためのガイドライン
-
コードを
ee/
に配置します:すべてのEnterprise Edition(EE) をee/
トップレベルディレクトリの内部に置いてください。残りのコードは、Community Edition の(CE) ファイルにできるだけ近づけるようにしてください。 -
テストを書くどのようなコードでもそうですが、EEの機能はリグレッションを防ぐためにテストカバレッジが優れている必要があります。すべての
ee/
コードには、ee/
に対応するテストが必要です。 -
ドキュメントを書く
doc/
ディレクトリにドキュメントを追加します。機能を説明し、該当する場合はスクリーンショットを含めてください。その機能がどのエディションに適用されるかを示してください。 - **
www-gitlab-com
プロジェクトに MR を提出します。**:EE機能リストに新機能を追加します。
SaaSのみの機能
SaaSのみに適用される機能(CustomersDotインテグレーションなど)を開発する場合は、以下のガイドラインを使用してください。
- アプリケーション設定を使用することをお勧めします。これにより、各 SaaS インスタンスが必要に応じて切り替えられるように、きめ細かな設定が可能になります。
- アプリケーション設定が不可能な場合は、
Gitlab.com?
のようなヘルパーを使用できます。ただし、ヘルパーを削除するエピックに記載されているように、これには欠点があります。- 他のSaaSインスタンスへのパフォーマンスと可用性の影響を考慮してください。例えば、GitLab JHはSaaSヘルパーをオーバーライドし、
Gitlab.com?
に対してtrueを返すようにします。
- 他のSaaSインスタンスへのパフォーマンスと可用性の影響を考慮してください。例えば、GitLab JHはSaaSヘルパーをオーバーライドし、
SaaSインスタンスのシミュレーション
ローカルで開発していて、インスタンスでSaaS(GitLab.com)版をシミュレートする必要がある場合:
-
この環境変数をエクスポートします:
export GITLAB_SIMULATE_SAAS=1
環境変数をローカルの GitLab インスタンスに渡す方法はたくさんあります。例えば、上記のスニペットでGDKのルートに
env.runit
。 -
Allow use of licensed EE featuresを有効にすると、プロジェクトのネームスペースの計画にその機能が含まれている場合にのみ、ライセンスされたEE機能をプロジェクトで利用できるようになります。
- 左のサイドバーで、Search を選択するか、次のページに進んでください。
- Admin Areaを選択します。
- 左サイドバーで、設定 > 一般を選択します。
- アカウントと制限」を展開します。
- Allow use of licensed EE features]チェックボックスを選択します。
- 変更を保存を選択します。
-
EE機能をテストするグループが実際にEEプランを使用していることを確認します:
- 左のサイドバーで、Search を選択するか、次のページに進んでください。
- Admin Areaを選択します。
- 左サイドバーで「概要」>「グループ」を選択します。
- 変更したいグループを確認し、「編集」を選択します。
-
権限とグループの機能までスクロールします。プラン] で、
Ultimate
を選択します。 - 変更を保存を選択します。
新しいEE機能の実装
GitLabプレミアムまたは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インスタンスレベルでシステム全体)に利用できますか?
-
GeoやDatabase Load Balancingのような機能はインスタンス全体で使用され、個々のユーザーネームスペースに制限することはできません。これらの機能はインスタンスライセンスで定義されています。これらの機能を
GLOBAL_FEATURES
に追加してください。
-
GeoやDatabase Load Balancingのような機能はインスタンス全体で使用され、個々のユーザーネームスペースに制限することはできません。これらの機能はインスタンスライセンスで定義されています。これらの機能を
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
-
-
オプション。グローバル機能が有料プランのネームスペースでも使用できる場合は、2 つの機能識別子を組み合わせて、管理者とグループ・ユーザーの両方を許可します。例えば
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
の spec helperstub_licensed_features
を使ってスタブすることができます。
GitLabをCEとして動作させるには、ee/
ディレクトリを削除するか、FOSS_ONLY
環境変数 をtrue
として評価されるものに設定します。テストを実行する場合も同様です (例えばFOSS_ONLY=1 yarn jest
)。
ライセンスされたGDKでCEインスタンスをシミュレートします。
GDKのライセンスを削除せずにCEインスタンスをシミュレートするには:
-
GDKのルートに
env.runit
:export FOSS_ONLY=1
-
その後、GDKを再起動してください:
gdk restart rails && gdk restart webpack
EEのインストールに戻したい場合は、env.runit
の行を削除し、ステップ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ジョブパイプラインのドキュメントをご覧ください。
バックエンドの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/
-prependedパスを追加するためです。これはビューにも当てはまります。
EEのみのバックエンド機能のテスト
CE に存在しない EE クラスをテストするには、ee/spec
ディレクトリに通常と同じように spec ファイルを作成しますが、2 番目のee/
サブディレクトリは作成しません。例えば、ee/app/models/vulnerability.rb
クラスのテストはee/spec/models/vulnerability_spec.rb
にあります。
デフォルトでは、specs/
にある spec に対してライセンス機能は無効になっています。ee/spec
ディレクトリにある Specs は、デフォルトで Starter ライセンスが初期化されています。
機能を効果的にテストするには、stub_licensed_features
ヘルパーなどを使用して明示的に機能を有効にする必要があります:
stub_licensed_features(my_awesome_feature_name: true)
EEバックエンドコードによるCE機能の拡張
既存のCE機能をベースとする機能については、EE
ネームスペースにモジュールを記述し、クラスが存在するファイルの最終行にあるCEクラスにインジェクトします。これにより、CEクラスには、モジュールをインジェクトする行が1行追加されるだけなので、CEからEEへのマージ時にコンフリクトが発生しにくくなります。たとえば、User
クラスにモジュールをプリペンドするには、次のようにします:
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_mod_with
,extend_mod_with
,include_mod_with
を使ってモジュール名を指定することもできます。 これらのメソッドは、モジュールそのものではなく、完全なモジュール名を含む_文字列を_引数にとります;
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
名前空間付きモジュールをテストするには、2番目のee/
サブディレクトリを含むee/spec
ディレクトリに、通常と同じように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
、overrider
メソッドをガードするためにoverride
。 -
overrider
、CE実装の途中に行が追加される場合は、CEメソッドをリファクタリングして小さなメソッドに分割する必要があります。または、CEでは空の “フック “メソッドを作成し、EEではEE固有の実装を行います。 -
元の実装にガード条項(例えば、
return unless condition
)が含まれている場合、オーバーライドするメソッド(つまり、オーバーライドするメソッドでsuper
を呼び出す)がいつ早期に停止したいのかが分からないため、メソッドをオーバーライドして動作を簡単に拡張することはできません。この場合、ただオーバーライドするのではなく、テンプレートメソッドパターンのように、元のメソッドを更新して、拡張したい別のメソッドを呼び出すようにします。例えば、このbase: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
プリペンドするときは、ee/
特定のサブディレクトリに置き、名前の衝突を避けるためにクラスやモジュールをmodule EE
で囲みます。
例えば、ApplicationController#after_sign_out_path_for
のCE実装をオーバーライドする場合:
def after_sign_out_path_for(resource)
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
メソッドを変更する代わりに、既存のファイルにprepend
を追加してください:
class ApplicationController < ActionController::Base
# ...
def after_sign_out_path_for(resource)
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
# ...
end
ApplicationController.prepend_mod_with('ApplicationController')
そして、ee/
サブディレクトリに、変更した実装の新しいファイルを作成します:
module EE
module ApplicationController
extend ::Gitlab::Utils::Override
override :after_sign_out_path_for
def after_sign_out_path_for(resource)
if Gitlab::Geo.secondary?
Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
else
super
end
end
end
end
CEクラスのメソッドのオーバーライド
ActiveSupport::Concern
、class_methods
のブロック内にextend ::Gitlab::Utils::Override
を置く以外は、クラス・メソッドも同様です:
module EE
module Groups
module GroupMembersController
extend ActiveSupport::Concern
class_methods do
extend ::Gitlab::Utils::Override
override :admin_not_required_endpoints
def admin_not_required_endpoints
super.concat(%i[update override])
end
end
end
end
end
自己記述ラッパー・メソッドを使う
メソッドの実装を変更することが不可能/論理的でない場合、自己記述的なメソッドでラップし、そのメソッドを使います。
例えば、GitLab-FOSSではシステムによって作成されるユーザーはUsers::Internal.ghost
だけですが、EEでは本当のユーザーではないボットユーザーが何種類か存在します。User#ghost?
の実装をオーバーライドするのは正しくないので、代わりにapp/models/user.rb
に#internal?
というメソッドを追加します:
def internal?
ghost?
end
EEでは、ee/app/models/ee/users.rb
の実装になります:
override :internal?
def internal?
super || bot?
end
のコードconfig/routes
config/routes.rb
にdraw :admin
を追加すると、アプリケーションはconfig/routes/admin.rb
にあるファイルをロードしようとし、ee/config/routes/admin.rb
にあるファイルもロードしようとします。
EEでは、少なくとも1つ、多くても2つのファイルをロードする必要があります。ファイルが見つからなければ、エラーが発生します。CEでは、EEルートが存在するかどうか分からないため、何も見つからなくてもエラーは発生しません。
つまり、特定のCEのルートファイルを拡張したい場合は、ee/config/routes
にある同じファイルを追加するだけです。EEのみのルートを追加したい場合は、draw :ee_only
をCEとEEの両方に置き、ee/config/routes/ee_only.rb
をEEに追加すれば、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
# 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
approver_group_ids
approver_ids
...
]
end
のコードapp/models/
EE-specific models shouldextend EE::Model
.
例えば、EE に特定のTanuki
モデルがある場合、それをee/app/models/ee/tanuki.rb
に配置します。
ActiveRecordenums
は全てFOSSで定義されるべきです。
のコードapp/views/
EEがCEビューに特定のビューコードを追加することは、非常によくある問題です。例えば、プロジェクトの設定ページにある承認者コードです。
緩和策
EE固有のコードのブロックは、パーシャルに移動する必要があります。これにより、インデントを方程式に追加したときに解決するのが楽しくないHAMLコードの大きな塊との衝突を避けることができます。
EE固有のビューは、ee/app/views/
、必要に応じてサブディレクトリを追加してください。
テストの拡張render_if_exists
通常のrender
を使う代わりに、render_if_exists
特定のパーシャルが見つからない場合は何もレンダリングしない , render_if_exists
を使うべきです。render_if_exists
これを使う render_if_exists
ことで、CEとEEで同じコードを維持したままCEに入れることができます。
この利点は
- CEコードを読みながら、EEビューを拡張している箇所について非常に明確なヒントが得られます。
デメリット
- 部分的な名前にタイプミスがあった場合、それは黙って無視されます。
注意点
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パーシャルを検索し、次にCEパーシャルを検索します。これらは同じ名前のすべてのパーシャルではなく、特定のパーシャルだけをレンダリングします。この利点を利用して、同じパーシャルパス (たとえば、projects/settings/archive
) が、CE (つまり、app/views/projects/settings/_archive.html.haml
) ではCEパーシャルを参照し、EE (つまり、ee/app/views/projects/settings/_archive.html.haml
) ではEEパーシャルを参照することができます。こうすることで、CEとEEで異なることを示すことができます。
しかし、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パーシャル(つまり、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
end
のコードapp/graphql/
EE固有の変異、リゾルバ、型をee/app/graphql/{mutations,resolvers,types}
に追加する必要があります。
CEの変異、リゾルバ、型をオーバーライドするには、ee/app/graphql/ee/{mutations,resolvers,types}
にファイルを作成し、prepended
ブロックに新しいコードを追加します。
例えば、CEにMutations::Tanukis::Create
という変異があり、新しい引数を追加したい場合、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 name'
end
end
end
end
end
のコードlib/
EE固有のロジックを最上位のEE
モジュール名前 EE
空間に配置します。モジュールのEE
下のクラスは EE
、通常のようにEE
名前空間を指定 EE
します。
たとえば、CE の LDAP クラスがlib/gitlab/ldap/
にある場合、EE 固有の LDAP クラスはee/lib/ee/gitlab/ldap
に配置します。
のコードlib/api/
EEの機能を1行のprepend_mod_with
で拡張するのは非常に厄介です。また、Grapeの異なる機能ごとに、それを拡張するための異なるストラテジーが必要になることもあります。異なる戦略を簡単に適用するには、EEモジュールでextend ActiveSupport::Concern
。
EEモジュールのファイルは、Extend CE features with EE backend codeの後に置きます。
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
を定義し、別のparams
定義でuse
を使用することで、EE で定義されたパラメータを含めることができます。ただし、EEでオーバーライドするためには、CEで最初に「インターフェース」を定義する必要があります。他の場所ではprepend_mod_with
、このようなことをする必要はありませんが、Grapeは内部が複雑なため、このようなことは簡単にはできませんでした。
例えば、EE用のオプションのパラメータがもう少しあるとします。パラメータをGrape::API::Instance
クラスからヘルパーモジュールに移動させ、クラスで使用される前に注入できるようにします。
module API
class Projects < Grape::API::Instance
helpers Helpers::ProjectsHelpers
end
end
このCE APIparams
:
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
EEroute_setting
EEモジュールでこれを拡張するのは非常に難しく、これは特定のルートのメタデータを保存しています。そう考えると、EEroute_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仕様にサンプルを追加することは避けてください。また、既存のCEサンプルも変更しないでください。EEがライセンスなしで実行されている場合でも、既存のCEサンプルはそのまま動作するはずです。
代わりに、EE specをee/spec
フォルダに配置します。
のコード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
とそれに対応する EEee/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';
フロントエンドに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 テストをee/spec/frontend/
に追加します。
ライセンス機能の有効化については、「Testing EE-only backend features」の注記を確認してください。
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 コンポーネントを使用して Vue テンプレートの差分を分離します。EEコンポーネントは非同期にインポートする必要があります。
これにより、EEではGitLabが正しいコンポーネントを読み込み、CEではGitLabが何もレンダリングしない空のコンポーネントを読み込みます。このコードはEEリポジトリに加えてCEリポジトリにも存在する必要があります。
CEコンポーネントはEE機能へのエントリーポイントとして機能します。EEコンポーネントを追加するには、ee/
ディレクトリを探し、import('ee_component/...')
で追加します:
<script>
// app/assets/javascripts/feature/components/form.vue
export default {
mixins: [glFeatureFlagMixin()],
components: {
// Import an EE component from CE
MyEeComponent: () => import('ee_component/components/my_ee_component.vue'),
},
};
</script>
<template>
<div>
<!-- ... -->
<my-ee-component/>
<!-- ... -->
</div>
</template>
glFeatures
をチェックして、Vue コンポーネントが保護されていることを確認します。コンポーネントがレンダリングされるのは、ライセンスが存在する場合のみです。
<script>
// ee/app/assets/javascripts/feature/components/special_component.vue
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
mixins: [glFeatureFlagMixin()],
computed: {
shouldRenderComponent() {
// 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>
おすすめの代替アプローチ(名前付き/スコープ付きスロット)
- スロットやスコープ付きスロットを使えば、ミキシンと同じことができます。EEコンポーネントだけが必要であれば、CEコンポーネントを作成する必要はありません。
- まず、EEテンプレートと機能をCEベースの上に装飾する必要がある場合に備えて、スロットをレンダリングできる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</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() {
// do something.
}
},
}
</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>
- 最後に、コンポーネントが必要な場所で、次のようにコンポーネントをrequireします。
import MyComponent from 'ee_else_ce/path/my_component'.vue
- こうすることで、CEまたはEE実装のどちらにも正しいコンポーネントが含まれるようになります。
同じ計算値に対して異なる結果が必要なEEコンポーネントの場合は、例にあるようにCEラッパーにpropsを渡すことができます。
-
EE子コンポーネント
- どのコンポーネントをロードするかをチェックするために非同期ロードを使用しているので、コンポーネントの名前を使用します。
-
EE追加HTML
- EEで余分なHTMLを持つテンプレートについては、それを新しいコンポーネントに移動し、
ee_else_ce
dynamic importを使用します。
- 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 = () => {
// ...
};
// ... 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 エイリアスを使用したモジュールのテスト
Frontend テストを記述する際、テスト対象のモジュールが .EE/CE エイリアスを使用して他のモジュールをインポートee_else_ce/...
し、それらのモジュールが関連するテストでも必要な場合、関連するテストは .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
// ...
// 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);
});
});
のSCSSコードassets/stylesheets
スタイルを追加するコンポーネントが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
で再生成することで解決できます。