Commit 1a1dd332 authored by Kamil Trzciński's avatar Kamil Trzciński

Add support for feature flag type Licensed

Our implicit licensed features are YAML stored.
parent e3dfc928
......@@ -9,7 +9,7 @@ module IssuableActions
before_action :check_destroy_confirmation!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
before_action only: :show do
push_frontend_feature_flag(:scoped_labels, default_enabled: true)
push_frontend_feature_flag(:scoped_labels, type: :licensed, default_enabled: true)
end
before_action do
push_frontend_feature_flag(:not_issuable_queries, @project, default_enabled: true)
......
......@@ -7,7 +7,7 @@ module Clusters
end
def feature_available?(feature)
::Feature.enabled?(feature, default_enabled: true)
::Feature.enabled?(feature, type: :licensed, default_enabled: true)
end
def flipper_id
......
......@@ -88,7 +88,7 @@ module Featurable
def feature_available?(feature, user)
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true)
return false unless ::Feature.enabled?(feature, user, type: :licensed, default_enabled: true)
get_permission(user, feature)
end
......
......@@ -181,6 +181,10 @@ class FeatureFlagOptionParser
$stderr.puts "URL needs to start with https://"
end
end
def read_default_enabled(options)
TYPES.dig(options.type, :default_enabled)
end
end
end
......@@ -226,7 +230,7 @@ class FeatureFlagCreator
'rollout_issue_url' => options.rollout_issue_url,
'group' => options.group.to_s,
'type' => options.type.to_s,
'default_enabled' => false
'default_enabled' => FeatureFlagOptionParser.read_default_enabled(options)
).strip
end
......
......@@ -61,6 +61,29 @@ Feature.disabled?(:my_ops_flag, project, type: ops)
push_frontend_feature_flag(:my_ops_flag, project, type: :ops)
```
### `licensed` type
`licensed` feature flags are implicit feature flags
that are one-to-one mapping of all licensed features.
These feature flags are used to temporarily disable
licensed features.
A `licensed` feature flag by its nature likely does not have a rollout
issue. The `licensed` feature flags by default has to be `default_enabled: true`
as current implementation does not allow otherwise.
This aspect is currently in works as part of
https://gitlab.com/gitlab-org/gitlab/-/issues/218667.
The `licensed` type have a dedicated set of functions to check
if a licensed feature is available in a context of a project or namespace.
This check validates license assigned to namespace and feature flag itself:
```ruby
# Check if feature flag is enabled
project.feature_available?(:my_licensed_feature)
namespace.feature_available?(:my_licensed_feature)
```
## Feature flag definition and validation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229161) in GitLab 13.3.
......@@ -290,13 +313,17 @@ The [`Project#feature_available?`](https://gitlab.com/gitlab-org/gitlab/blob/4cc
[`License.feature_available?`](https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300) (EE) methods all implicitly check for
a by default enabled feature flag with the same name as the provided argument.
You'd still want to use an explicit `Feature.enabled?` check if your new feature
isn't gated by a License or Plan.
**An important side-effect of the implicit feature flags mentioned above is that
unless the feature is explicitly disabled or limited to a percentage of users,
the feature flag check will default to `true`.**
CAUTION: **Caution:**
Due to current limitations of `feature_available?` the YAML definition
for licensed feature flags accepts only `default_enabled: true`.
This is subject to be improved as part of the https://gitlab.com/gitlab-org/gitlab/-/issues/218667.
#### Beta/alpha licensed feature flags
This is relevant when developing the feature using
[several smaller merge requests](https://about.gitlab.com/handbook/values/#make-small-merge-requests), or when the feature is considered to be an
[alpha or beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga), and
......@@ -311,6 +338,23 @@ GitLab.com and self-managed instances, you should use the
method, according to our [definitions](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga). This ensures the feature is disabled unless the feature flag is
_explicitly_ enabled.
CAUTION: **Caution:**
If the `beta_feature_available?` or `alpha_feature_available?` is used
the YAML definition of feature flag needs to use `default_enabled: [false, true]`
as the usage of the feature flag is undefined. These methods are subject to be removed as part of the discusions https://gitlab.com/gitlab-org/gitlab/-/issues/218667.
The resulting YAML should be similar to this:
```yaml
name: scoped_labels
group: group::memory
type: licensed
# The `default_enabled:` is undefined
# as `feature_available?` uses `default_enabled: true`
# as `beta_feature_available?` uses `default_enabled: false`
default_enabled: [false, true]
```
### Feature groups
Feature groups must be defined statically in `lib/feature.rb` (in the
......
......@@ -12,7 +12,7 @@ module EE
def push_wip_limits
# This is pushing a licensed Feature to the frontend.
push_frontend_feature_flag(:wip_limits, default_enabled: true) if parent.feature_available?(:wip_limits)
push_frontend_feature_flag(:wip_limits, type: :licensed, default_enabled: true) if parent.feature_available?(:wip_limits)
end
end
end
......@@ -6,7 +6,9 @@ class Groups::Analytics::ApplicationController < ApplicationController
private
def self.check_feature_flag(flag, *args)
before_action(*args) { render_404 unless Feature.enabled?(flag, default_enabled: Gitlab::Analytics.feature_enabled_by_default?(flag)) }
before_action(*args) do
render_404 unless Gitlab::Analytics.feature_enabled?(flag)
end
end
def self.increment_usage_counter(counter_klass, counter, *args)
......
......@@ -163,9 +163,8 @@ module EE
private
def project_has_subscriptions?
return false unless ::Feature.enabled?(:ci_project_subscriptions, project)
project.downstream_projects.any?
project.beta_feature_available?(:ci_project_subscriptions) &&
project.downstream_projects.any?
end
def merge_train_ref?
......
......@@ -126,7 +126,7 @@ module EE
# it. This is the case when we're ready to enable a feature for anyone
# with the correct license.
def beta_feature_available?(feature)
::Feature.enabled?(feature) ? feature_available?(feature) : ::Feature.enabled?(feature, self)
::Feature.enabled?(feature, type: :licensed) ? feature_available?(feature) : ::Feature.enabled?(feature, self, type: :licensed)
end
alias_method :alpha_feature_available?, :beta_feature_available?
......@@ -136,7 +136,7 @@ module EE
override :feature_available?
def feature_available?(feature)
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, default_enabled: true)
return false unless ::Feature.enabled?(feature, type: :licensed, default_enabled: true)
available_features = strong_memoize(:feature_available) do
Hash.new do |h, f|
......
......@@ -306,7 +306,7 @@ module EE
# it. This is the case when we're ready to enable a feature for anyone
# with the correct license.
def beta_feature_available?(feature)
::Feature.enabled?(feature) ? feature_available?(feature) : ::Feature.enabled?(feature, self)
::Feature.enabled?(feature, type: :licensed) ? feature_available?(feature) : ::Feature.enabled?(feature, self, type: :licensed)
end
alias_method :alpha_feature_available?, :beta_feature_available?
......@@ -704,7 +704,7 @@ module EE
def licensed_feature_available?(feature, user = nil)
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true)
return false unless ::Feature.enabled?(feature, user, type: :licensed, default_enabled: true)
available_features = strong_memoize(:licensed_feature_available) do
Hash.new do |h, f|
......
......@@ -378,7 +378,7 @@ class License < ApplicationRecord
return false if trial? && expired?
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, default_enabled: true)
return false unless ::Feature.enabled?(feature, type: :licensed, default_enabled: true)
features.include?(feature)
end
......
......@@ -8,7 +8,7 @@ module PersonalAccessTokens
end
def execute
return unless ::Feature.enabled?(:personal_access_token_expiration_policy, default_enabled: true)
return unless ::Feature.enabled?(:personal_access_token_expiration_policy, type: :licensed, default_enabled: true)
return unless PersonalAccessToken.expiration_enforced?
return unless expiration_date && user_affected?
......
......@@ -21,8 +21,18 @@ module Gitlab
CYCLE_ANALYTICS_FEATURE_FLAG => true
}.freeze
FEATURE_FLAGS_TYPE = {
# TODO: it seems that we use a licensed "feature"
PRODUCTIVITY_ANALYTICS_FEATURE_FLAG => :licensed,
GROUP_COVERAGE_REPORTS_FEATURE_FLAG => :licensed,
GROUP_MERGE_REQUEST_ANALYTICS_FEATURE_FLAG => :licensed,
CYCLE_ANALYTICS_FEATURE_FLAG => :development
}.freeze
def self.any_features_enabled?
FEATURE_FLAGS.any? { |flag| Feature.enabled?(flag, default_enabled: feature_enabled_by_default?(flag)) }
FEATURE_FLAGS.any? do |flag|
feature_enabled?(flag)
end
end
def self.cycle_analytics_enabled?
......@@ -50,7 +60,9 @@ module Gitlab
end
def self.feature_enabled?(feature)
Feature.enabled?(feature, default_enabled: feature_enabled_by_default?(feature))
Feature.enabled?(feature,
type: FEATURE_FLAGS_TYPE.fetch(feature),
default_enabled: feature_enabled_by_default?(feature))
end
end
end
......@@ -389,7 +389,7 @@ RSpec.describe Ci::Pipeline do
context 'when feature is available' do
before do
stub_feature_flags(ci_project_subscriptions: true)
stub_licensed_features(ci_project_subscriptions: true)
end
it 'schedules the trigger downstream subscriptions worker' do
......
......@@ -8,12 +8,14 @@ class Feature
module Shared
# optional: defines if a on-disk definition is required for this feature flag type
# rollout_issue: defines if `bin/feature-flag` asks for rollout issue
# default_enabled: defines a default state of a feature flag when created by `bin/feature-flag`
# example: usage being shown when exception is raised
TYPES = {
development: {
description: 'Short lived, used to enable unfinished code to be deployed',
optional: true,
rollout_issue: true,
default_enabled: false,
example: <<-EOS
Feature.enabled?(:my_feature_flag, project)
Feature.enabled?(:my_feature_flag, project, type: :development)
......@@ -24,10 +26,21 @@ class Feature
description: "Long-lived feature flags that control operational aspects of GitLab's behavior",
optional: true,
rollout_issue: false,
default_enabled: false,
example: <<-EOS
Feature.enabled?(:my_ops_flag, type: ops)
push_frontend_feature_flag?(:my_ops_flag, project, type: :ops)
EOS
},
licensed: {
description: 'Forever, used like a config to enable rollout licensed features for certain users',
optional: true,
rollout_issue: false,
default_enabled: true,
example: <<-EOS
project.feature_available?(:my_licensed_feature)
namespace.feature_available?(:my_licensed_feature)
EOS
}
}.freeze
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment