12 KiB
Guidelines for implementing Enterprise Edition feature
- Write the code and the tests.: As with any code, EE features should have good test coverage to prevent regressions.
- Write documentation.: Add documentation to the
doc/
directory. Describe the feature and include screenshots, if applicable. - Submit a MR to the
www-gitlab-com
project.: Add the new feature to the [EE features list][ee-features-list].
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
We want a single code base eventually, but before we reach the goal, we still need to merge changes from GitLab CE to EE. To help us get there, we should make sure that we no longer edit CE files in place in order to implement EE features.
Instead, all EE codes should be put inside the ee/
top-level directory, and
tests should be put inside spec/ee/
. We don't use ee/spec
for now due to
technical limitation. The rest of codes should be as close as to the CE files.
EE-only features
If the feature being developed is not present in any form in CE, we don't
need to put the codes under EE
namespace. For example, an EE model could
go into: ee/app/models/awesome.rb
using Awesome
as the class name. This
is applied not only to models. Here's a list of other examples:
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 features based on CE features
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.
Since the module would require an EE
namespace, the file should also be
put in an ee/
sub-directory. For example, we want to extend the user model
in EE, so we have a module called ::EE::User
put inside
ee/app/models/ee/user.rb
.
This is also not just applied to models. Here's a list of other examples:
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
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:
- you should always add a
raise NotImplementedError unless defined?(super)
guard clause in the "overrider" method to ensure that if the method gets renamed in CE, the EE override won't be silently forgotten. - when the "overrider" would add a line in the middle of the CE implementation, you should refactor the CE method and split it in smaller methods. Or create a "hook" method that is empty in CE, and with the EE-specific implementation in EE.
- when the original implementation contains a guard clause (e.g.
return unless condition
), we cannot easily extend the behaviour by overriding the method, because we can't know when the overridden method (i.e. callingsuper
in the overriding method) would want to stop early. In this case, we shouldn't just override it, but update the original method to make it call the other method we want to extend, like a template method pattern. For example, given this base:
Instead of just overridingclass Base def execute return unless enabled? # ... # ... end end
Base#execute
, we should update it and extract the behaviour into another method:
Then we're free to override thatclass Base def execute return unless enabled? do_something end private def do_something # ... # ... end end
do_something
without worrying about the guards:
This would require updating CE first, or make sure this is back ported to CE.module EE::Base def do_something # Follow the above pattern to call super and extend it end end
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;
}
}