Enterprise Editionの機能を実装するためのガイドライン

  • コードをee/に配置します:すべてのEnterprise Edition(EE) をee/ トップレベルディレクトリの内部に置いてください。残りのコードは、Community Edition の(CE) ファイルにできるだけ近づけるようにしてください。
  • テストを書くどのようなコードでもそうですが、EEの機能はリグレッションを防ぐためにテストカバレッジが優れている必要があります。すべてのee/ コードには、ee/ に対応するテストが必要です。
  • ドキュメントを書くdoc/ ディレクトリにドキュメントを追加します。機能を説明し、該当する場合はスクリーンショットを含めてください。その機能がどのエディションに適用されるかを示してください。
  • ** www-gitlab-com プロジェクトに MR を提出します。**:EE機能リストに新機能を追加します。

SaaSのみの機能

SaaSのみに適用される機能(CustomersDotインテグレーションなど)を開発する場合は、以下のガイドラインを使用してください。

  1. アプリケーション設定を使用することをお勧めします。これにより、各 SaaS インスタンスが必要に応じて切り替えられるように、きめ細かな設定が可能になります。
  2. アプリケーション設定が不可能な場合は、Gitlab.com? のようなヘルパーを使用できます。ただし、ヘルパーを削除するエピックに記載されているように、これには欠点があります。
    1. 他のSaaSインスタンスへのパフォーマンスと可用性の影響を考慮してください。例えば、GitLab JHはSaaSヘルパーをオーバーライドし、Gitlab.com?に対してtrueを返すようにします。

SaaSインスタンスのシミュレーション

ローカルで開発していて、インスタンスでSaaS(GitLab.com)版をシミュレートする必要がある場合:

  1. この環境変数をエクスポートします:

    export GITLAB_SIMULATE_SAAS=1
    

    環境変数をローカルの GitLab インスタンスに渡す方法はたくさんあります。例えば、上記のスニペットでGDKのルートにenv.runit

  2. Allow use of licensed EE featuresを有効にすると、プロジェクトのネームスペースの計画にその機能が含まれている場合にのみ、ライセンスされたEE機能をプロジェクトで利用できるようになります。

    1. 左のサイドバーで、Search を選択するか、次のページに進んでください。
    2. Admin Areaを選択します。
    3. 左サイドバーで、設定 > 一般を選択します。
    4. アカウントと制限」を展開します。
    5. Allow use of licensed EE features]チェックボックスを選択します。
    6. 変更を保存を選択します。
  3. EE機能をテストするグループが実際にEEプランを使用していることを確認します:

    1. 左のサイドバーで、Search を選択するか、次のページに進んでください。
    2. Admin Areaを選択します。
    3. 左サイドバーで「概要」>「グループ」を選択します。
    4. 変更したいグループを確認し、「編集」を選択します。
    5. 権限とグループの機能までスクロールします。プラン] で、Ultimateを選択します。
    6. 変更を保存を選択します。

新しいEE機能の実装

GitLabプレミアムまたはGitLab Ultimateライセンスの機能を開発する場合は、以下の手順で新機能を追加または拡張してください。

GitLabライセンス機能はee/app/models/gitlab_subscriptions/features.rbに追加されます。 このファイルをどのように変更するかを決めるには、まずあなたの機能がどのように私たちのライセンスに適合するかをプロダクトマネージャーと相談してください。

以下の質問を参考にしてください:

  1. これは新しい機能ですか、それとも既存のライセンス機能を拡張するのですか?
    • 機能がすでに存在する場合は、features.rb を変更する必要はありませんが、既存の機能識別子を探して保護する必要があります。
    • 新しい機能の場合は、my_feature_name のような識別子を決めて、features.rb ファイルに追加します。
  2. これはGitLab Premiumか GitLab Ultimateの機能ですか?
    • 機能を使用するプランに応じて、機能識別子をPREMIUM_FEATURES またはULTIMATE_FEATURES に追加してください。
  3. この機能はグローバル(GitLabインスタンスレベルでシステム全体)に利用できますか?
    • GeoDatabase Load Balancingのような機能はインスタンス全体で使用され、個々のユーザーネームスペースに制限することはできません。これらの機能はインスタンスライセンスで定義されています。これらの機能をGLOBAL_FEATURESに追加してください。

EE 機能のガード

ライセンスされた機能は、ライセンスされたユーザーのみが使用できます。ユーザーがその機能にアクセスできるかどうかを判断するために、チェックまたはガードを追加する必要があります。

ライセンス機能をガードするには

  1. ee/app/models/gitlab_subscriptions/features.rb で機能識別子を探します。
  2. 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
    
  3. オプション。グローバル機能が有料プランのネームスペースでも使用できる場合は、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インスタンスをシミュレートするには:

  1. GDKのルートにenv.runit

    export FOSS_ONLY=1
    
  2. その後、GDKを再起動してください:

    gdk restart rails && gdk restart webpack
    

EEのインストールに戻したい場合は、env.runit の行を削除し、ステップ2を繰り返します。

CEとして機能仕様を実行

CEとして機能仕様を実行する場合、バックエンドとフロントエンドのエディションが一致していることを確認する必要があります。そのためには

  1. FOSS_ONLY=1 環境変数を設定します:

    export FOSS_ONLY=1
    
  2. GDKを起動します:

    gdk start
    
  3. 機能仕様の実行

    bin/rspec spec/features/<path_to_your_spec>
    

FOSSコンテキストでのCIパイプラインの実行

デフォルトでは、開発用のマージリクエストパイプラインはEEコンテキストでのみ実行されます。FOSS と EE で異なる機能を開発する場合は、FOSS コンテキストでもパイプラインを実行することをお勧めします。

両方のコンテキストでパイプラインを実行するには、マージリクエストに~"pipeline:run-as-if-foss" ラベルを追加します。

詳しくはAs-if-FOSSジョブパイプラインのドキュメントをご覧ください。

バックエンドの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

prependextendinclude などのメソッドは使用しないでください。代わりに、prepend_modextend_modinclude_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::Overrideoverrider メソッドをガードするために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::Concernclass_methods のブロック内にextend ::Gitlab::Utils::Override を置く以外は、クラス・メソッドも同様です:

module EE
  module Groups
    module GroupMembersController
      extend ActiveSupport::Concern

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

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

自己記述ラッパー・メソッドを使う

メソッドの実装を変更することが不可能/論理的でない場合、自己記述的なメソッドでラップし、そのメソッドを使います。

例えば、GitLab-FOSSではシステムによって作成されるユーザーはUsers::Internal.ghost だけですが、EEでは本当のユーザーではないボットユーザーが何種類か存在します。User#ghost? の実装をオーバーライドするのは正しくないので、代わりにapp/models/user.rb#internal? というメソッドを追加します:

def internal?
  ghost?
end

EEでは、ee/app/models/ee/users.rb の実装になります:

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

のコードconfig/routes

config/routes.rbdraw :admin を追加すると、アプリケーションはconfig/routes/admin.rb にあるファイルをロードしようとし、ee/config/routes/admin.rb にあるファイルもロードしようとします。

EEでは、少なくとも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のアクション/キーワードを分離。インスタンスProjectsControllerparams.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

renderrender_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>
note
絶対に必要な場合を除き、ミキシンを使用しないでください。別のパターンを見つけてください。
  • スロットやスコープ付きスロットを使えば、ミキシンと同じことができます。EEコンポーネントだけが必要であれば、CEコンポーネントを作成する必要はありません。
  1. まず、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>
  1. 次に、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>
  1. 最後に、コンポーネントが必要な場所で、次のようにコンポーネントを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を使用します。

他のJSコードの拡張

JSファイルを拡張するには、以下の手順を実行します:

  1. ee_else_ce ヘルパーを使用します。その EE 唯一のコードはee/ フォルダーの内部になければなりません。
    1. EEのみのEEファイルを作成し、CEを拡張します。
    2. 拡張できない関数内部のコードについては、コードを新しいファイルに移動し、ee_else_ce ヘルパーを使用します:
  import eeCode from 'ee_else_ce/ee_code';

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

    eeCode();

    return test;
  }

場合によっては、アプリケーション内の他のロジックを拡張する必要があります。JSモジュールを拡張するには、EEバージョンのファイルを作成し、カスタムロジックで拡張します:

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

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

// ... 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.jsonapp/assets/images/icons.svg のコンフリクトは、それらのアセットをyarn run svgで再生成することで解決できます。