Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-02 09:09:00 +00:00
parent 3c2841692e
commit 6aab18704a
29 changed files with 1028 additions and 44 deletions

View File

@ -357,7 +357,6 @@ linters:
- "ee/app/views/notify/unapproved_merge_request_email.html.haml"
- "ee/app/views/oauth/geo_auth/error.html.haml"
- "ee/app/views/projects/commits/_mirror_status.html.haml"
- "ee/app/views/projects/jobs/_shared_runner_limit_warning.html.haml"
- "ee/app/views/projects/merge_requests/_approvals_count.html.haml"
- "ee/app/views/projects/merge_requests/widget/open/_geo.html.haml"
- "ee/app/views/projects/mirrors/_mirrored_repositories_count.html.haml"

View File

@ -6,7 +6,7 @@ $brand-info: $blue-500;
$brand-warning: $orange-500;
$brand-danger: $red-500;
$border-radius-base: 3px !default;
$border-radius-base: $gl-border-radius-base;
$modal-body-bg: $white;
$input-border: $border-color;

View File

@ -5,4 +5,6 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
#js-job-vue-app{ data: jobs_data }

291
bin/feature-flag Executable file
View File

@ -0,0 +1,291 @@
#!/usr/bin/env ruby
#
# Generate a feature flag entry file in the correct location.
#
# Automatically stages the file and amends the previous commit if the `--amend`
# argument is used.
require 'optparse'
require 'yaml'
require 'fileutils'
require 'cgi'
require_relative '../lib/feature/shared' unless defined?(Feature::Shared)
Options = Struct.new(
:name,
:type,
:group,
:ee,
:amend,
:dry_run,
:force,
:introduced_by_url,
:rollout_issue_url
)
module FeatureFlagHelpers
Abort = Class.new(StandardError)
Done = Class.new(StandardError)
def capture_stdout(cmd)
output = IO.popen(cmd, &:read)
fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
output
end
def fail_with(message)
raise Abort, "\e[31merror\e[0m #{message}"
end
end
class FeatureFlagOptionParser
extend FeatureFlagHelpers
extend ::Feature::Shared
class << self
def parse(argv)
options = Options.new
parser = OptionParser.new do |opts|
opts.banner = "Usage: #{__FILE__} [options] <feature-flag>\n\n"
# Note: We do not provide a shorthand for this in order to match the `git
# commit` interface
opts.on('--amend', 'Amend the previous commit') do |value|
options.amend = value
end
opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
options.force = value
end
opts.on('-m', '--introduced-by-url [string]', String, 'URL to Merge Request introducing Feature Flag') do |value|
options.introduced_by_url = value
end
opts.on('-i', '--rollout-issue-url [string]', String, 'URL to Issue rolling out Feature Flag') do |value|
options.rollout_issue_url = value
end
opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
options.dry_run = value
end
opts.on('-g', '--group [string]', String, "The group introducing a feature flag, like: `group::apm`") do |value|
options.group = value if value.start_with?('group::')
end
opts.on('-t', '--type [string]', String, "The category of the feature flag, valid options are: #{TYPES.keys.map(&:to_s).join(', ')}") do |value|
options.type = value.to_sym if TYPES[value.to_sym]
end
opts.on('-e', '--ee', 'Generate a feature flag entry for GitLab EE') do |value|
options.ee = value
end
opts.on('-h', '--help', 'Print help message') do
$stdout.puts opts
raise Done.new
end
end
parser.parse!(argv)
unless argv.one?
$stdout.puts parser.help
$stdout.puts
raise Abort, 'Feature flag name is required'
end
# Name is a first name
options.name = argv.first
options
end
def read_group
$stdout.puts ">> Please specify the group introducing feature flag, like `group::apm`:"
loop do
$stdout.print "\n?> "
group = $stdin.gets.strip
group = nil if group.empty?
return group if group.nil? || group.start_with?('group::')
$stderr.puts "Group needs to include `group::`"
end
end
def read_type
$stdout.puts ">> Please specify the type of your feature flag:"
$stdout.puts
TYPES.each do |type, data|
$stdout.puts "#{type.to_s.rjust(15)}#{' '*6}#{data[:description]}"
end
loop do
$stdout.print "\n?> "
type = $stdin.gets.strip.to_sym
return type if TYPES[type]
$stderr.puts "Invalid type specified '#{type}'"
end
end
def read_issue_url(options)
return unless TYPES.dig(options.type, :rollout_issue)
url = "https://gitlab.com/gitlab-org/gitlab/-/issues/new"
title = "[Feature flag] Rollout of `#{options.name}`"
description = File.read('.gitlab/issue_templates/Feature Flag Roll Out.md')
description.sub!(':feature_name', options.name)
issue_new_url = url + "?" +
"issue[title]=" + CGI.escape(title) + "&"
# TODO: We should be able to pick `issueable_template`
# + "issue[description]=" + CGI.escape(description)
$stdout.puts ">> Open this URL and fill the rest of details:"
$stdout.puts issue_new_url
$stdout.puts
$stdout.puts ">> Paste URL here, or enter to skip:"
loop do
$stdout.print "\n?> "
created_url = $stdin.gets.strip
created_url = nil if created_url.empty?
return created_url if created_url.nil? || created_url.start_with?('https://')
$stderr.puts "URL needs to start with https://"
end
end
end
end
class FeatureFlagCreator
include FeatureFlagHelpers
attr_reader :options
def initialize(options)
@options = options
end
def execute
assert_feature_branch!
assert_name!
assert_existing_feature_flag!
# Read type from $stdin unless is already set
options.type ||= FeatureFlagOptionParser.read_type
options.group ||= FeatureFlagOptionParser.read_group
options.rollout_issue_url ||= FeatureFlagOptionParser.read_issue_url(options)
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
$stdout.puts contents
unless options.dry_run
write
amend_commit if options.amend
end
if editor
system("#{editor} '#{file_path}'")
end
end
private
def contents
YAML.dump(
'name' => options.name,
'introduced_by_url' => options.introduced_by_url,
'rollout_issue_url' => options.rollout_issue_url,
'group' => options.group.to_s,
'type' => options.type.to_s,
'default_enabled' => false
).strip
end
def write
FileUtils.mkdir_p(File.dirname(file_path))
File.write(file_path, contents)
end
def editor
ENV['EDITOR']
end
def amend_commit
fail_with "git add failed" unless system(*%W[git add #{file_path}])
Kernel.exec(*%w[git commit --amend])
end
def assert_feature_branch!
return unless branch_name == 'master'
fail_with "Create a branch first!"
end
def assert_existing_feature_flag!
existing_path = all_feature_flag_names[options.name]
return unless existing_path
return if options.force
fail_with "#{existing_path} already exists! Use `--force` to overwrite."
end
def assert_name!
return if options.name.match(/\A[a-z0-9_-]+\Z/)
fail_with "Provide a name for the feature flag that is [a-z0-9_-]"
end
def file_path
feature_flags_paths.last
.sub('**', options.type.to_s)
.sub('*.yml', options.name + '.yml')
end
def all_feature_flag_names
@all_feature_flag_names ||=
feature_flags_paths.map do |glob_path|
Dir.glob(glob_path).map do |path|
[File.basename(path, '.yml'), path]
end
end.flatten(1).to_h
end
def feature_flags_paths
paths = []
paths << File.join('config', 'feature_flags', '**', '*.yml')
paths << File.join('ee', 'config', 'feature_flags', '**', '*.yml') if ee?
paths
end
def ee?
options.ee
end
def branch_name
@branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
end
end
if $0 == __FILE__
begin
options = FeatureFlagOptionParser.parse(ARGV)
FeatureFlagCreator.new(options).execute
rescue FeatureFlagHelpers::Abort => ex
$stderr.puts ex.message
exit 1
rescue FeatureFlagHelpers::Done
exit
end
end
# vim: ft=ruby

View File

@ -0,0 +1,5 @@
---
title: Fix border-radius-base SCSS value
merge_request: 35740
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update snippet statistics after project import
merge_request: 35730
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move merge_requests_users metric to stage section
merge_request: 35593
author:
type: changed

View File

@ -0,0 +1,5 @@
# This needs to be loaded after
# config/initializers/0_inject_enterprise_edition_module.rb
Feature.register_feature_groups
Feature.register_definitions

View File

@ -1 +0,0 @@
Feature.register_feature_groups

View File

@ -908,6 +908,21 @@ result as you did at the start. For example:
Note that `enforced="true"` means that authentication is being enforced.
## Direct Git access bypassing Gitaly
While it is possible to access Gitaly repositories stored on disk directly with a Git client,
it is not advisable because Gitaly is being continuously improved and changed. Theses improvements may invalidate assumptions, resulting in performance degradation, instability, and even data loss.
Gitaly has optimizations, such as the
[`info/refs` advertisement cache](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/design_diskcache.md),
that rely on Gitaly controlling and monitoring access to repositories via the
official gRPC interface. Likewise, Praefect has optimizations, such as fault
tolerance and distributed reads, that depend on the gRPC interface and
database to determine repository state.
For these reasons, **accessing repositories directly is done at your own risk
and is not supported**.
## Direct access to Git in GitLab
Direct access to Git uses code in GitLab known as the "Rugged patches".

View File

@ -32,7 +32,6 @@ pipelines for merge requests take precedence over the other regular pipelines.
To enable pipelines for merge requests:
- You must have maintainer [permissions](../../user/permissions.md).
- Your repository must be a GitLab repository, not an
[external repository](../ci_cd_for_external_repos/index.md).
- [In GitLab 11.10 and later](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25504),

View File

@ -2465,8 +2465,6 @@ The `stop_review_app` job is **required** to have the following keywords defined
- `when` - [reference](#when)
- `environment:name`
- `environment:action`
- `stage` should be the same as the `review_app` in order for the environment
to stop automatically when the branch is deleted
Additionally, both jobs should have matching [`rules`](../yaml/README.md#onlyexcept-basic)
or [`only/except`](../yaml/README.md#onlyexcept-basic) configuration. In the example

View File

@ -453,6 +453,28 @@ are:
To reduce unnecessary differences between two distribution methods, Omnibus and
CNG **should always use the same Go version**.
### Supporting multiple Go versions
Individual Golang-projects need to support multiple Go versions for the following reasons:
1. When a new Go release is out, we should start integrating it into the CI pipelines to verify compatibility with the new compiler.
1. We must support the [Omnibus official Go version](#updating-go-version), which may be behind the latest minor release.
1. When Omnibus switches Go version, we still may need to support the old one for security backports.
These 3 requirements may easily be satisfied by keeping support for the 3 latest minor versions of Go.
It's ok to drop support for the oldest Go version and support only 2 latest releases,
if this is enough to support backports to the last 3 GitLab minor releases.
Example:
In case we want to drop support for `go 1.11` in GitLab `12.10`, we need to verify which Go versions we are using in `12.9`, `12.8`, and `12.7`.
We will not consider the active milestone, `12.10`, because a backport for `12.7` will be required in case of a critical security release.
1. If both [Omnibus and CNG](#updating-go-version) were using Go `1.12` since GitLab `12.7`, then we safely drop support for `1.11`.
1. If Omnibus or CNG were using `1.11` in GitLab `12.7`, then we still need to keep support for Go `1.11` for easier backporting of security fixes.
## Secure Team standards and style guidelines
The following are some style guidelines that are specific to the Secure Team.

View File

@ -175,9 +175,9 @@ Jobs can have an `urgency` attribute set, which can be `:high`,
| **Urgency** | **Queue Scheduling Target** | **Execution Latency Requirement** |
|--------------|-----------------------------|------------------------------------|
| `:high` | 100 milliseconds | p50 of 1 second, p99 of 10 seconds |
| `:low` | 1 minute | Maximum run time of 1 hour |
| `:throttled` | None | Maximum run time of 1 hour |
| `:high` | 10 seconds | p50 of 1 second, p99 of 10 seconds |
| `:low` | 1 minute | Maximum run time of 5 minutes |
| `:throttled` | None | Maximum run time of 5 minutes |
To set a job's urgency, use the `urgency` class method:

View File

@ -665,6 +665,7 @@ appear to be associated to any of the services running, since they all appear to
| `ci_triggers` | `usage_activity_by_stage` | `verify` | | | Triggers enabled |
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled |
| `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled |
| `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request |
| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running |
| `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data |
| `node_memory_total_bytes` | `topology > nodes` | `enablement` | | | The total available memory of this node |

View File

@ -54,12 +54,14 @@ class Feature
# unless set explicitly. The default is `disabled`
# TODO: remove the `default_enabled:` and read it from the `defintion_yaml`
# check: https://gitlab.com/gitlab-org/gitlab/-/issues/30228
def enabled?(key, thing = nil, default_enabled: false)
def enabled?(key, thing = nil, type: :development, default_enabled: false)
if check_feature_flags_definition?
if thing && !thing.respond_to?(:flipper_id)
raise InvalidFeatureFlagError,
"The thing '#{thing.class.name}' for feature flag '#{key}' needs to include `FeatureGate` or implement `flipper_id`"
end
Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled)
end
# During setup the database does not exist yet. So we haven't stored a value
@ -75,9 +77,9 @@ class Feature
!default_enabled || Feature.persisted_name?(feature.name) ? feature.enabled?(thing) : true
end
def disabled?(key, thing = nil, default_enabled: false)
def disabled?(key, thing = nil, type: :development, default_enabled: false)
# we need to make different method calls to make it easy to mock / define expectations in test mode
thing.nil? ? !enabled?(key, default_enabled: default_enabled) : !enabled?(key, thing, default_enabled: default_enabled)
thing.nil? ? !enabled?(key, type: type, default_enabled: default_enabled) : !enabled?(key, thing, type: type, default_enabled: default_enabled)
end
def enable(key, thing = true)
@ -129,6 +131,12 @@ class Feature
def register_feature_groups
end
def register_definitions
return unless check_feature_flags_definition?
Feature::Definition.load_all!
end
private
def flipper

137
lib/feature/definition.rb Normal file
View File

@ -0,0 +1,137 @@
# frozen_string_literal: true
class Feature
class Definition
include ::Feature::Shared
attr_reader :path
attr_reader :attributes
PARAMS.each do |param|
define_method(param) do
attributes[param]
end
end
def initialize(path, opts = {})
@path = path
@attributes = {}
# assign nil, for all unknown opts
PARAMS.each do |param|
@attributes[param] = opts[param]
end
end
def key
name.to_sym
end
def validate!
unless name.present?
raise Feature::InvalidFeatureFlagError, "Feature flag is missing name"
end
unless path.present?
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing path"
end
unless type.present?
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing type. Ensure to update #{path}"
end
unless Definition::TYPES.include?(type.to_sym)
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' type '#{type}' is invalid. Ensure to update #{path}"
end
unless File.basename(path, ".yml") == name
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' has an invalid path: '#{path}'. Ensure to update #{path}"
end
unless File.basename(File.dirname(path)) == type
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' has an invalid type: '#{path}'. Ensure to update #{path}"
end
if default_enabled.nil?
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing default_enabled. Ensure to update #{path}"
end
end
def valid_usage!(type_in_code:, default_enabled_in_code:)
unless Array(type).include?(type_in_code.to_s)
# Raise exception in test and dev
raise Feature::InvalidFeatureFlagError, "The `type:` of `#{key}` is not equal to config: " \
"#{type_in_code} vs #{type}. Ensure to use valid type in #{path} or ensure that you use " \
"a valid syntax: #{TYPES.dig(type, :example)}"
end
# We accept an array of defaults as some features are undefined
# and have `default_enabled: true/false`
unless Array(default_enabled).include?(default_enabled_in_code)
# Raise exception in test and dev
raise Feature::InvalidFeatureFlagError, "The `default_enabled:` of `#{key}` is not equal to config: " \
"#{default_enabled_in_code} vs #{default_enabled}. Ensure to update #{path}"
end
end
def to_h
attributes
end
class << self
def paths
@paths ||= [Rails.root.join('config', 'feature_flags', '**', '*.yml')]
end
def definitions
@definitions ||= {}
end
def load_all!
definitions.clear
paths.each do |glob_path|
load_all_from_path!(glob_path)
end
definitions
end
def valid_usage!(key, type:, default_enabled:)
if definition = definitions[key.to_sym]
definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled)
elsif type_definition = self::TYPES[type]
raise InvalidFeatureFlagError, "Missing feature definition for `#{key}`" unless type_definition[:optional]
else
raise InvalidFeatureFlagError, "Unknown feature flag type used: `#{type}`"
end
end
private
def load_from_file(path)
definition = File.read(path)
definition = YAML.safe_load(definition)
definition.deep_symbolize_keys!
self.new(path, definition).tap(&:validate!)
rescue => e
raise Feature::InvalidFeatureFlagError, "Invalid definition for `#{path}`: #{e.message}"
end
def load_all_from_path!(glob_path)
Dir.glob(glob_path).each do |path|
definition = load_from_file(path)
if previous = definitions[definition.key]
raise InvalidFeatureFlagError, "Feature flag '#{definition.key}' is already defined in '#{previous.path}'"
end
definitions[definition.key] = definition
end
end
end
end
end
Feature::Definition.prepend_if_ee('EE::Feature::Definition')

33
lib/feature/shared.rb Normal file
View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# This file can contain only simple constructs as it is shared between:
# 1. `Pure Ruby`: `bin/feature-flag`
# 2. `GitLab Rails`: `lib/feature/definition.rb`
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
# 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,
example: <<-EOS
Feature.enabled?(:my_feature_flag)
Feature.enabled?(:my_feature_flag, type: :development)
EOS
}
}.freeze
PARAMS = %i[
name
default_enabled
type
introduced_by_url
rollout_issue_url
group
].freeze
end
end

View File

@ -42,6 +42,8 @@ module Gitlab
snippet.repository.expire_exists_cache
raise SnippetRepositoryError, _("Invalid repository bundle for snippet with id %{snippet_id}") % { snippet_id: snippet.id }
else
Snippets::UpdateStatisticsService.new(snippet).execute
end
end

View File

@ -159,8 +159,7 @@ module Gitlab
usage_counters,
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage,
merge_requests_usage(last_28_days_time_period)
container_expiration_policies_usage
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
@ -405,23 +404,19 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests_usage(time_period)
def merge_requests_users(time_period)
query =
Event
.where(target_type: Event::TARGET_TYPES[:merge_request].to_s)
.where(time_period)
merge_request_users = distinct_count(
distinct_count(
query,
:author_id,
batch_size: 5_000, # Based on query performance, this is the optimal batch size.
start: User.minimum(:id),
finish: User.maximum(:id)
)
{
merge_requests_users: merge_request_users
}
end
# rubocop: enable CodeReuse/ActiveRecord
@ -477,9 +472,10 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
# Omitted because no user, creator or author associated: `lfs_objects`, `pool_repositories`, `web_hooks`
def usage_activity_by_stage_create(time_period)
{}
{}.tap do |h|
h[:merge_requests_users] = merge_requests_users(time_period) if time_period.present?
end
end
# Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links`

View File

@ -19552,6 +19552,9 @@ msgstr ""
msgid "Revoke"
msgstr ""
msgid "Revoked"
msgstr ""
msgid "Revoked impersonation token %{token_name}!"
msgstr ""

View File

@ -0,0 +1,191 @@
# frozen_string_literal: true
require 'spec_helper'
load File.expand_path('../../bin/feature-flag', __dir__)
RSpec.describe 'bin/feature-flag' do
using RSpec::Parameterized::TableSyntax
describe FeatureFlagCreator do
let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url] }
let(:options) { FeatureFlagOptionParser.parse(argv) }
let(:creator) { described_class.new(options) }
let(:existing_flag) { File.join('config', 'feature_flags', 'development', 'existing-feature-flag.yml') }
before do
# create a dummy feature flag
FileUtils.mkdir_p(File.dirname(existing_flag))
File.write(existing_flag, '{}')
# ignore writes
allow(File).to receive(:write).and_return(true)
# ignore stdin
allow($stdin).to receive(:gets).and_raise('EOF')
# ignore Git commands
allow(creator).to receive(:branch_name) { 'feature-branch' }
end
after do
FileUtils.rm_f(existing_flag)
end
subject { creator.execute }
it 'properly creates a feature flag' do
expect(File).to receive(:write).with(
File.join('config', 'feature_flags', 'development', 'feature-flag-name.yml'),
anything)
expect do
subject
end.to output(/name: feature-flag-name/).to_stdout
end
context 'when running on master' do
it 'requires feature branch' do
expect(creator).to receive(:branch_name) { 'master' }
expect { subject }.to raise_error(FeatureFlagHelpers::Abort, /Create a branch first/)
end
end
context 'validates feature flag name' do
where(:argv, :ex) do
%w[.invalid.feature.flag] | /Provide a name for the feature flag that is/
%w[existing-feature-flag] | /already exists!/
end
with_them do
it do
expect { subject }.to raise_error(ex)
end
end
end
end
describe FeatureFlagOptionParser do
describe '.parse' do
where(:param, :argv, :result) do
:name | %w[foo] | 'foo'
:amend | %w[foo --amend] | true
:force | %w[foo -f] | true
:force | %w[foo --force] | true
:ee | %w[foo -e] | true
:ee | %w[foo --ee] | true
:introduced_by_url | %w[foo -m https://url] | 'https://url'
:introduced_by_url | %w[foo --introduced-by-url https://url] | 'https://url'
:rollout_issue_url | %w[foo -i https://url] | 'https://url'
:rollout_issue_url | %w[foo --rollout-issue-url https://url] | 'https://url'
:dry_run | %w[foo -n] | true
:dry_run | %w[foo --dry-run] | true
:type | %w[foo -t development] | :development
:type | %w[foo --type development] | :development
:type | %w[foo -t invalid] | nil
:type | %w[foo --type invalid] | nil
:group | %w[foo -g group::memory] | 'group::memory'
:group | %w[foo --group group::memory] | 'group::memory'
:group | %w[foo -g invalid] | nil
:group | %w[foo --group invalid] | nil
end
with_them do
it do
options = described_class.parse(Array(argv))
expect(options.public_send(param)).to eq(result)
end
end
it 'missing feature flag name' do
expect do
expect { described_class.parse(%w[--amend]) }.to output(/Feature flag name is required/).to_stdout
end.to raise_error(FeatureFlagHelpers::Abort)
end
it 'parses -h' do
expect do
expect { described_class.parse(%w[foo -h]) }.to output(/Usage:/).to_stdout
end.to raise_error(FeatureFlagHelpers::Done)
end
end
describe '.read_type' do
let(:type) { 'development' }
it 'reads type from $stdin' do
expect($stdin).to receive(:gets).and_return(type)
expect do
expect(described_class.read_type).to eq(:development)
end.to output(/specify the type/).to_stdout
end
context 'invalid type given' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
expect($stdin).to receive(:gets).and_return(type)
expect($stdin).to receive(:gets).and_raise('EOF')
expect do
expect { described_class.read_type }.to raise_error(/EOF/)
end.to output(/specify the type/).to_stdout
.and output(/Invalid type specified/).to_stderr
end
end
end
describe '.read_group' do
let(:group) { 'group::memory' }
it 'reads type from $stdin' do
expect($stdin).to receive(:gets).and_return(group)
expect do
expect(described_class.read_group).to eq('group::memory')
end.to output(/specify the group/).to_stdout
end
context 'invalid group given' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
expect($stdin).to receive(:gets).and_return(type)
expect($stdin).to receive(:gets).and_raise('EOF')
expect do
expect { described_class.read_group }.to raise_error(/EOF/)
end.to output(/specify the group/).to_stdout
.and output(/Group needs to include/).to_stderr
end
end
end
describe '.rollout_issue_url' do
let(:options) { OpenStruct.new(name: 'foo', type: :development) }
let(:url) { 'https://issue' }
it 'reads type from $stdin' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_issue_url(options)).to eq('https://issue')
end.to output(/Paste URL here/).to_stdout
end
context 'invalid URL given' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
expect($stdin).to receive(:gets).and_return(type)
expect($stdin).to receive(:gets).and_raise('EOF')
expect do
expect { described_class.read_issue_url(options) }.to raise_error(/EOF/)
end.to output(/Paste URL here/).to_stdout
.and output(/URL needs to start/).to_stderr
end
end
end
end
end

View File

@ -63,11 +63,17 @@ describe('Monitoring store actions', () => {
let store;
let state;
let dispatch;
let commit;
beforeEach(() => {
store = createStore({ getters });
state = store.state.monitoringDashboard;
mock = new MockAdapter(axios);
commit = jest.fn();
dispatch = jest.fn();
jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
const q = new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
@ -200,12 +206,8 @@ describe('Monitoring store actions', () => {
// Metrics dashboard
describe('fetchDashboard', () => {
let dispatch;
let commit;
const response = metricsDashboardResponse;
beforeEach(() => {
dispatch = jest.fn();
commit = jest.fn();
state.dashboardEndpoint = '/dashboard';
});
@ -292,14 +294,6 @@ describe('Monitoring store actions', () => {
});
describe('receiveMetricsDashboardSuccess', () => {
let commit;
let dispatch;
beforeEach(() => {
commit = jest.fn();
dispatch = jest.fn();
});
it('stores groups', () => {
const response = metricsDashboardResponse;
receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response });
@ -359,13 +353,8 @@ describe('Monitoring store actions', () => {
// Metrics
describe('fetchDashboardData', () => {
let commit;
let dispatch;
beforeEach(() => {
jest.spyOn(Tracking, 'event');
commit = jest.fn();
dispatch = jest.fn();
state.timeRange = defaultTimeRange;
});

View File

@ -0,0 +1,209 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Feature::Definition do
let(:attributes) do
{ name: 'feature_flag',
type: 'development',
default_enabled: true }
end
let(:path) { File.join('development', 'feature_flag.yml') }
let(:definition) { described_class.new(path, attributes) }
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
describe '#key' do
subject { definition.key }
it 'returns a symbol from name' do
is_expected.to eq(:feature_flag)
end
end
describe '#validate!' do
using RSpec::Parameterized::TableSyntax
where(:param, :value, :result) do
:name | nil | /Feature flag is missing name/
:path | nil | /Feature flag 'feature_flag' is missing path/
:type | nil | /Feature flag 'feature_flag' is missing type/
:type | 'invalid' | /Feature flag 'feature_flag' type 'invalid' is invalid/
:path | 'development/invalid.yml' | /Feature flag 'feature_flag' has an invalid path/
:path | 'invalid/feature_flag.yml' | /Feature flag 'feature_flag' has an invalid type/
:default_enabled | nil | /Feature flag 'feature_flag' is missing default_enabled/
end
with_them do
let(:params) { attributes.merge(path: path) }
before do
params[param] = value
end
it do
expect do
described_class.new(
params[:path], params.except(:path)
).validate!
end.to raise_error(result)
end
end
end
describe '#valid_usage!' do
context 'validates type' do
it 'raises exception for invalid type' do
expect { definition.valid_usage!(type_in_code: :invalid, default_enabled_in_code: false) }
.to raise_error(/The `type:` of `feature_flag` is not equal to config/)
end
end
context 'validates default enabled' do
it 'raises exception for different value' do
expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) }
.to raise_error(/The `default_enabled:` of `feature_flag` is not equal to config/)
end
end
end
describe '.paths' do
it 'returns at least one path' do
expect(described_class.paths).not_to be_empty
end
end
describe '.load_from_file' do
it 'properly loads a definition from file' do
expect(File).to receive(:read).with(path) { yaml_content }
expect(described_class.send(:load_from_file, path).attributes)
.to eq(definition.attributes)
end
context 'for missing file' do
let(:path) { 'missing/feature-flag/file.yml' }
it 'raises exception' do
expect do
described_class.send(:load_from_file, path)
end.to raise_error(/Invalid definition for/)
end
end
context 'for invalid definition' do
it 'raises exception' do
expect(File).to receive(:read).with(path) { '{}' }
expect do
described_class.send(:load_from_file, path)
end.to raise_error(/Feature flag is missing name/)
end
end
end
describe '.load_all!' do
let(:store1) { Dir.mktmpdir('path1') }
let(:store2) { Dir.mktmpdir('path2') }
before do
allow(described_class).to receive(:paths).and_return(
[
File.join(store1, '**', '*.yml'),
File.join(store2, '**', '*.yml')
]
)
end
it "when there's no feature flags a list of definitions is empty" do
expect(described_class.load_all!).to be_empty
end
it "when there's a single feature flag it properly loads them" do
write_feature_flag(store1, path, yaml_content)
expect(described_class.load_all!).to be_one
end
it "when the same feature flag is stored multiple times raises exception" do
write_feature_flag(store1, path, yaml_content)
write_feature_flag(store2, path, yaml_content)
expect { described_class.load_all! }
.to raise_error(/Feature flag 'feature_flag' is already defined/)
end
it "when one of the YAMLs is invalid it does raise exception" do
write_feature_flag(store1, path, '{}')
expect { described_class.load_all! }
.to raise_error(/Feature flag is missing name/)
end
after do
FileUtils.rm_rf(store1)
FileUtils.rm_rf(store2)
end
def write_feature_flag(store, path, content)
path = File.join(store, path)
dir = File.dirname(path)
FileUtils.mkdir_p(dir)
File.write(path, content)
end
end
describe '.valid_usage!' do
before do
allow(described_class).to receive(:definitions) do
{ definition.key => definition }
end
end
context 'when a known feature flag is used' do
it 'validates it usage' do
expect(definition).to receive(:valid_usage!)
described_class.valid_usage!(:feature_flag, type: :development, default_enabled: false)
end
end
context 'when an unknown feature flag is used' do
context 'for a type that is required to have all feature flags registered' do
before do
stub_const('Feature::Shared::TYPES', {
development: { optional: false }
})
end
it 'raises exception' do
expect do
described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false)
end.to raise_error(/Missing feature definition for `unknown_feature_flag`/)
end
end
context 'for a type that is optional' do
before do
stub_const('Feature::Shared::TYPES', {
development: { optional: true }
})
end
it 'does not raise exception' do
expect do
described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false)
end.not_to raise_error
end
end
context 'for an unknown type' do
it 'raises exception' do
expect do
described_class.valid_usage!(:unknown_feature_flag, type: :unknown_type, default_enabled: false)
end.to raise_error(/Unknown feature flag type used: `unknown_type`/)
end
end
end
end
end

View File

@ -242,6 +242,36 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
end
context 'validates usage of feature flag with YAML definition' do
let(:definition) do
Feature::Definition.new('development/my_feature_flag.yml',
name: 'my_feature_flag',
type: 'development',
default_enabled: false
).tap(&:validate!)
end
before do
allow(Feature::Definition).to receive(:definitions) do
{ definition.key => definition }
end
end
it 'when usage is correct' do
expect { described_class.enabled?(:my_feature_flag) }.not_to raise_error
end
it 'when invalid type is used' do
expect { described_class.enabled?(:my_feature_flag, type: :licensed) }
.to raise_error(/The `type:` of/)
end
it 'when invalid default_enabled is used' do
expect { described_class.enabled?(:my_feature_flag, default_enabled: true) }
.to raise_error(/The `default_enabled:` of/)
end
end
end
describe '.disable?' do

View File

@ -35,6 +35,12 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
end
end
it 'does not call snippet update statistics service' do
expect(Snippets::UpdateStatisticsService).not_to receive(:new).with(snippet)
restorer.restore
end
context 'when the repository creation fails' do
it 'returns false' do
allow_any_instance_of(Gitlab::BackgroundMigration::BackfillSnippetRepositories).to receive(:perform_by_ids).and_return(nil)
@ -66,6 +72,10 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
before do
expect(exporter.save).to be_truthy
allow_next_instance_of(Snippets::RepositoryValidationService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success)
end
end
context 'when it is valid' do
@ -115,5 +125,19 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
end
end
end
it 'refreshes snippet statistics' do
expect(snippet.statistics.commit_count).to be_zero
expect(snippet.statistics.file_count).to be_zero
expect(snippet.statistics.repository_size).to be_zero
expect(Snippets::UpdateStatisticsService).to receive(:new).with(snippet).and_call_original
restorer.restore
expect(snippet.statistics.commit_count).not_to be_zero
expect(snippet.statistics.file_count).not_to be_zero
expect(snippet.statistics.repository_size).not_to be_zero
end
end
end

View File

@ -77,6 +77,22 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
context 'for create' do
it 'include usage_activity_by_stage data' do
expect(described_class.uncached_data[:usage_activity_by_stage][:create])
.not_to include(
:merge_requests_users
)
end
it 'includes monthly usage_activity_by_stage data' do
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:create])
.to include(
:merge_requests_users
)
end
end
it 'ensures recorded_at is set before any other usage data calculation' do
%i(alt_usage_data redis_usage_data distinct_count count).each do |method|
expect(described_class).not_to receive(method)
@ -662,7 +678,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
describe '.merge_requests_usage' do
describe '.merge_requests_users' do
let(:time_period) { { created_at: 2.days.ago..Time.current } }
let(:merge_request) { create(:merge_request) }
let(:other_user) { create(:user) }
@ -679,9 +695,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
it 'returns the distinct count of users using merge requests (via events table) within the specified time period' do
expect(described_class.merge_requests_usage(time_period)).to eq(
merge_requests_users: 2
)
expect(described_class.merge_requests_users(time_period)).to eq(2)
end
end

View File

@ -155,6 +155,9 @@ RSpec.configure do |config|
config.before(:suite) do
Timecop.safe_mode = true
TestEnv.init
# Reload all feature flags definitions
Feature.register_definitions
end
config.after(:all) do

View File

@ -78,7 +78,6 @@ module UsageDataHelpers
labels
lfs_objects
merge_requests
merge_requests_users
milestone_lists
milestones
notes