GitLab Documentation

Guidelines for implementing Enterprise Edition feature

Act as CE when unlicensed

Since the implementation of GitLab CE features to work with unlicensed EE instance GitLab Enterprise Edition should work like GitLab Community Edition when no license is active. So EE features always should be guarded by project.feature_available? or group.feature_available? (or License.feature_available? if it is a system-wide feature).

CE specs should remain untouched as much as possible and extra specs should be added for EE. Licensed features can be stubbed using the spec helper stub_licensed_features in EE::LicenseHelpers.

Separation of EE code

Merging changes from GitLab CE to EE can result in numerous conflicts. To reduce conflicts, EE code should be separated in to the EE module as much as possible.

When referencing constants outside of the EE namespace from within it, you should always use absolute constants - e.g., ::User instead of User. This will prevent ::EE::User from being picked instead of ::User. Follow this rule even if the constant doesn't exist in the EE namespace at present - it may be added in the future.

Classes vs. Module Mixins

If the feature being developed is not present in any form in CE, separation is easy - build the class entirely in the EE namespace.

For features that build on existing CE features, write a module in the EE namespace and prepend it in the CE class. This makes conflicts less likely to happen during CE to EE merges because only one line is added to the CE class - the prepend line.

Adding EE-only files

Place EE-only controllers, finders, helpers, mailers, models, policies, serializers/entities, services, validators and workers in the top-level EE module namespace, and in the ee/ specific sub-directory:

This applies to both EE-only classes and EE module mixins.

Overriding CE methods

To override a method present in the CE codebase, use prepend. It lets you override a method in a class with a method from a module, while still having access the class's implementation with super.

There are a few gotchas with it:

When prepending, place them in the ee/ specific sub-directory, and wrap class or module in module EE to avoid naming conflicts.

For example to override the CE implementation of 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

Instead of modifying the method in place, you should add prepend to the existing file:

class ApplicationController < ActionController::Base
  prepend EE::ApplicationController
  # ...

  def after_sign_out_path_for(resource)
    current_application_settings.after_sign_out_path.presence || new_user_session_path
  end

  # ...
end

And create a new file in the ee/ sub-directory with the altered implementation:

module EE
  class ApplicationController
    def after_sign_out_path_for(resource)
      raise NotImplementedError unless defined?(super)

      if Gitlab::Geo.secondary?
        Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
      else
        super
      end
    end
  end
end

Use self-descriptive wrapper methods

When it's not possible/logical to modify the implementation of a method. Wrap it in a self-descriptive method and use that method.

For example, in CE only an admin is allowed to access all private projects/groups, but in EE also an auditor has full private access. It would be incorrect to override the implementation of User#admin?, so instead add a method full_private_access? to app/models/users.rb. The implementation in CE will be:

def full_private_access?
  admin?
end

In EE, the implementation ee/app/models/ee/users.rb would be:

def full_private_access?
  raise NotImplementedError unless defined?(super)
  super || auditor?
end

In lib/gitlab/visibility_level.rb this method is used to return the allowed visibilty levels:

def levels_for_user(user = nil)
  if user.full_private_access?
    [PRIVATE, INTERNAL, PUBLIC]
  elsif # ...
end

See CE MR and EE MR for full implementation details.

Code in app/controllers/

In controllers, the most common type of conflict is with before_action that has a list of actions in CE but EE adds some actions to that list.

The same problem often occurs for params.require / params.permit calls.

Mitigations

Separate CE and EE actions/keywords. For instance for params.require in ProjectsController:

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

In the EE::ProjectsController module:

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

Code in app/models/

EE-specific models should extend EE::Model.

For example, if EE has a specific Tanuki model, you would place it in ee/app/models/ee/tanuki.rb.

Code in app/views/

It's a very frequent problem that EE is adding some specific view code in a CE view. For instance the approval code in the project's settings page.

Mitigations

Blocks of code that are EE-specific should be moved to partials. This avoids conflicts with big chunks of HAML code that that are not fun to resolve when you add the indentation to the equation.

EE-specific views should be placed in ee/app/views/ee/, using extra sub-directories if appropriate.

Code in lib/

Place EE-specific logic in the top-level EE module namespace. Namespace the class beneath the EE module just as you would normally.

For example, if CE has LDAP classes in lib/gitlab/ldap/ then you would place EE-specific LDAP classes in ee/lib/ee/gitlab/ldap.

Code in spec/

When you're testing EE-only features, avoid adding examples to the existing CE specs. Also do no change existing CE examples, since they should remain working as-is when EE is running without a license.

Instead place EE specs in the spec/ee/spec folder.

JavaScript code in assets/javascripts/

To separate EE-specific JS-files we can also move the files into an ee folder.

For example there can be an app/assets/javascripts/protected_branches/protected_branches_bundle.js and an EE counterpart ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js.

That way we can create a separate webpack bundle in webpack.config.js:

    protected_branches:    '~/protected_branches',
    ee_protected_branches: 'ee/protected_branches/protected_branches_bundle.js',

With the separate bundle in place, we can decide which bundle to load inside the view, using the page_specific_javascript_bundle_tag helper.

- content_for :page_specific_javascripts do
  = page_specific_javascript_bundle_tag('protected_branches')

SCSS code in assets/stylesheets

To separate EE-specific styles in SCSS files, if a component you're adding styles for is limited to only EE, it is better to have a separate SCSS file in appropriate directory within app/assets/stylesheets.

In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill, e.g. a text style of some component is different for EE. In such cases, styles are usually kept in stylesheet that is common for both CE and EE, and it is wise to isolate such ruleset from rest of CE rules (along with adding comment describing the same) to avoid conflicts during CE to EE merge.

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 styles */
.section-body.ee-section-body {
  .section-title {
    background: $gl-header-color-cyan;
  }
}