Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f5a72705e4
commit
155fb78b9a
|
@ -25,19 +25,19 @@ export default {
|
||||||
},
|
},
|
||||||
stickinessOptions: [
|
stickinessOptions: [
|
||||||
{
|
{
|
||||||
value: 'DEFAULT',
|
value: 'default',
|
||||||
text: __('Available ID'),
|
text: __('Available ID'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'USERID',
|
value: 'userId',
|
||||||
text: __('User ID'),
|
text: __('User ID'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'SESSIONID',
|
value: 'sessionId',
|
||||||
text: __('Session ID'),
|
text: __('Session ID'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'RANDOM',
|
value: 'random',
|
||||||
text: __('Random'),
|
text: __('Random'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -37,7 +37,6 @@ module Namespaces
|
||||||
module Traversal
|
module Traversal
|
||||||
module Linear
|
module Linear
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include LinearScopes
|
|
||||||
|
|
||||||
UnboundedSearch = Class.new(StandardError)
|
UnboundedSearch = Class.new(StandardError)
|
||||||
|
|
||||||
|
@ -45,6 +44,14 @@ module Namespaces
|
||||||
before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
|
before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
|
||||||
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
|
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
|
||||||
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
|
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
|
||||||
|
|
||||||
|
scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
|
||||||
|
# When filtering namespaces by the traversal_ids column to compile a
|
||||||
|
# list of namespace IDs, it's much faster to reference the ID in
|
||||||
|
# traversal_ids than the primary key ID column.
|
||||||
|
# WARNING This scope must be used behind a linear query feature flag
|
||||||
|
# such as `use_traversal_ids`.
|
||||||
|
scope :as_ids, -> { select('traversal_ids[array_length(traversal_ids, 1)] AS id') }
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_traversal_ids?
|
def sync_traversal_ids?
|
||||||
|
@ -157,14 +164,20 @@ module Namespaces
|
||||||
Namespace.lock.select(:id).where(id: roots).order(id: :asc).load
|
Namespace.lock.select(:id).where(id: roots).order(id: :asc).load
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Make sure we drop the STI `type = 'Group'` condition for better performance.
|
||||||
|
# Logically equivalent so long as hierarchies remain homogeneous.
|
||||||
|
def without_sti_condition
|
||||||
|
self.class.unscope(where: :type)
|
||||||
|
end
|
||||||
|
|
||||||
# Search this namespace's lineage. Bound inclusively by top node.
|
# Search this namespace's lineage. Bound inclusively by top node.
|
||||||
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
|
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
|
||||||
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
|
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
|
||||||
|
|
||||||
skope = self.class.without_sti_condition
|
skope = without_sti_condition
|
||||||
|
|
||||||
if top
|
if top
|
||||||
skope = skope.where("traversal_ids @> ('{?}')", top.id)
|
skope = skope.traversal_ids_contains("{#{top.id}}")
|
||||||
end
|
end
|
||||||
|
|
||||||
if bottom
|
if bottom
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Namespaces
|
|
||||||
module Traversal
|
|
||||||
module LinearScopes
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
class_methods do
|
|
||||||
# When filtering namespaces by the traversal_ids column to compile a
|
|
||||||
# list of namespace IDs, it can be faster to reference the ID in
|
|
||||||
# traversal_ids than the primary key ID column.
|
|
||||||
def as_ids
|
|
||||||
return super unless use_traversal_ids?
|
|
||||||
|
|
||||||
select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
|
|
||||||
end
|
|
||||||
|
|
||||||
def self_and_descendants
|
|
||||||
return super unless use_traversal_ids?
|
|
||||||
|
|
||||||
without_dups = self_and_descendants_with_duplicates
|
|
||||||
.select('DISTINCT on(namespaces.id) namespaces.*')
|
|
||||||
|
|
||||||
# Wrap the `SELECT DISTINCT on(....)` with a normal query so we
|
|
||||||
# retain expected Rails behavior. Otherwise count and other
|
|
||||||
# aggregates won't work.
|
|
||||||
unscoped.without_sti_condition.from(without_dups, :namespaces)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self_and_descendant_ids
|
|
||||||
return super unless use_traversal_ids?
|
|
||||||
|
|
||||||
self_and_descendants_with_duplicates.select('DISTINCT namespaces.id')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Make sure we drop the STI `type = 'Group'` condition for better performance.
|
|
||||||
# Logically equivalent so long as hierarchies remain homogeneous.
|
|
||||||
def without_sti_condition
|
|
||||||
unscope(where: :type)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def use_traversal_ids?
|
|
||||||
Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self_and_descendants_with_duplicates
|
|
||||||
base_ids = select(:id)
|
|
||||||
|
|
||||||
unscoped
|
|
||||||
.without_sti_condition
|
|
||||||
.from("namespaces, (#{base_ids.to_sql}) base")
|
|
||||||
.where('namespaces.traversal_ids @> ARRAY[base.id]')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -4,7 +4,6 @@ module Namespaces
|
||||||
module Traversal
|
module Traversal
|
||||||
module Recursive
|
module Recursive
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include RecursiveScopes
|
|
||||||
|
|
||||||
def root_ancestor
|
def root_ancestor
|
||||||
return self if parent.nil?
|
return self if parent.nil?
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Namespaces
|
|
||||||
module Traversal
|
|
||||||
module RecursiveScopes
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
class_methods do
|
|
||||||
def as_ids
|
|
||||||
select('id')
|
|
||||||
end
|
|
||||||
|
|
||||||
def self_and_descendants
|
|
||||||
Gitlab::ObjectHierarchy.new(all).base_and_descendants
|
|
||||||
end
|
|
||||||
alias_method :recursive_self_and_descendants, :self_and_descendants
|
|
||||||
|
|
||||||
def self_and_descendant_ids
|
|
||||||
self_and_descendants.as_ids
|
|
||||||
end
|
|
||||||
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -16,7 +16,7 @@ module Operations
|
||||||
STRATEGY_USERWITHID => ['userIds'].freeze
|
STRATEGY_USERWITHID => ['userIds'].freeze
|
||||||
}.freeze
|
}.freeze
|
||||||
USERID_MAX_LENGTH = 256
|
USERID_MAX_LENGTH = 256
|
||||||
STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze
|
STICKINESS_SETTINGS = %w[default userId sessionId random].freeze
|
||||||
|
|
||||||
self.table_name = 'operations_strategies'
|
self.table_name = 'operations_strategies'
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ class ProjectFeature < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
default_value_for(:container_registry_access_level, allows_nil: false) do |feature|
|
default_value_for(:container_registry_access_level) do |feature|
|
||||||
if gitlab_config_features.container_registry
|
if gitlab_config_features.container_registry
|
||||||
ENABLED
|
ENABLED
|
||||||
else
|
else
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class OperationsFeatureFlagsCorrectFlexibleRolloutValues < ActiveRecord::Migration[6.1]
|
||||||
|
STICKINESS = { "USERID" => "userId", "RANDOM" => "random", "SESSIONID" => "sessionId", "DEFAULT" => "default" }.freeze
|
||||||
|
|
||||||
|
def up
|
||||||
|
STICKINESS.each do |before, after|
|
||||||
|
update_statement = <<-SQL
|
||||||
|
UPDATE operations_strategies
|
||||||
|
SET parameters = parameters || jsonb_build_object('stickiness', '#{quote_string(after)}')
|
||||||
|
WHERE name = 'flexibleRollout' AND parameters->>'stickiness' = '#{quote_string(before)}'
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute(update_statement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
STICKINESS.each do |before, after|
|
||||||
|
update_statement = <<-SQL
|
||||||
|
UPDATE operations_strategies
|
||||||
|
SET parameters = parameters || jsonb_build_object('stickiness', '#{quote_string(before)}')
|
||||||
|
WHERE name = 'flexibleRollout' AND parameters->>'stickiness' = '#{quote_string(after)}'
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute(update_statement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
cea8e51f6917be9ad43280fba9f8e7d9b9db1f508e249d9f5df792e43c0b8313
|
|
@ -212,7 +212,7 @@
|
||||||
"babel-plugin-istanbul": "^6.0.0",
|
"babel-plugin-istanbul": "^6.0.0",
|
||||||
"chalk": "^2.4.1",
|
"chalk": "^2.4.1",
|
||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
"commander": "^2.18.0",
|
"commander": "^2.20.3",
|
||||||
"custom-jquery-matchers": "^2.1.0",
|
"custom-jquery-matchers": "^2.1.0",
|
||||||
"docdash": "^1.0.2",
|
"docdash": "^1.0.2",
|
||||||
"eslint": "7.31.0",
|
"eslint": "7.31.0",
|
||||||
|
@ -264,7 +264,7 @@
|
||||||
"webpack-dev-server": "^3.11.2",
|
"webpack-dev-server": "^3.11.2",
|
||||||
"xhr-mock": "^2.5.1",
|
"xhr-mock": "^2.5.1",
|
||||||
"yarn-check-webpack-plugin": "^1.2.0",
|
"yarn-check-webpack-plugin": "^1.2.0",
|
||||||
"yarn-deduplicate": "^1.1.1"
|
"yarn-deduplicate": "^3.1.0"
|
||||||
},
|
},
|
||||||
"blockedDependencies": {
|
"blockedDependencies": {
|
||||||
"bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.html#bootstrapvue"
|
"bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.html#bootstrapvue"
|
||||||
|
|
|
@ -84,10 +84,13 @@ module QA
|
||||||
# Get issue comments
|
# Get issue comments
|
||||||
#
|
#
|
||||||
# @return [Array]
|
# @return [Array]
|
||||||
def comments(auto_paginate: false)
|
def comments(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_comments_path)) unless auto_paginate
|
return parse_body(api_get_from(api_comments_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url)
|
auto_paginated_response(
|
||||||
|
Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url,
|
||||||
|
attempts: attempts
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -160,10 +160,13 @@ module QA
|
||||||
# Get MR comments
|
# Get MR comments
|
||||||
#
|
#
|
||||||
# @return [Array]
|
# @return [Array]
|
||||||
def comments(auto_paginate: false)
|
def comments(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_comments_path)) unless auto_paginate
|
return parse_body(api_get_from(api_comments_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url)
|
auto_paginated_response(
|
||||||
|
Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url,
|
||||||
|
attempts: attempts
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -264,21 +264,24 @@ module QA
|
||||||
|
|
||||||
result = parse_body(response)
|
result = parse_body(response)
|
||||||
|
|
||||||
Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed"
|
if result[:import_status] == "failed"
|
||||||
|
Runtime::Logger.error("Import failed: #{result[:import_error]}")
|
||||||
|
Runtime::Logger.error("Failed relations: #{result[:failed_relations]}")
|
||||||
|
end
|
||||||
|
|
||||||
result[:import_status]
|
result[:import_status]
|
||||||
end
|
end
|
||||||
|
|
||||||
def commits(auto_paginate: false)
|
def commits(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_commits_path)) unless auto_paginate
|
return parse_body(api_get_from(api_commits_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(request_url(api_commits_path, per_page: '100'))
|
auto_paginated_response(request_url(api_commits_path, per_page: '100'), attempts: attempts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_requests(auto_paginate: false)
|
def merge_requests(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate
|
return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'))
|
auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'), attempts: attempts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_request_with_title(title)
|
def merge_request_with_title(title)
|
||||||
|
@ -302,10 +305,10 @@ module QA
|
||||||
parse_body(response)
|
parse_body(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
def repository_branches(auto_paginate: false)
|
def repository_branches(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate
|
return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'))
|
auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'), attempts: attempts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def repository_tags
|
def repository_tags
|
||||||
|
@ -328,22 +331,22 @@ module QA
|
||||||
parse_body(response)
|
parse_body(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
def issues(auto_paginate: false)
|
def issues(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_issues_path)) unless auto_paginate
|
return parse_body(api_get_from(api_issues_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(request_url(api_issues_path, per_page: '100'))
|
auto_paginated_response(request_url(api_issues_path, per_page: '100'), attempts: attempts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels(auto_paginate: false)
|
def labels(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_labels_path)) unless auto_paginate
|
return parse_body(api_get_from(api_labels_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(request_url(api_labels_path, per_page: '100'))
|
auto_paginated_response(request_url(api_labels_path, per_page: '100'), attempts: attempts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def milestones(auto_paginate: false)
|
def milestones(auto_paginate: false, attempts: 0)
|
||||||
return parse_body(api_get_from(api_milestones_path)) unless auto_paginate
|
return parse_body(api_get_from(api_milestones_path)) unless auto_paginate
|
||||||
|
|
||||||
auto_paginated_response(request_url(api_milestones_path, per_page: '100'))
|
auto_paginated_response(request_url(api_milestones_path, per_page: '100'), attempts: attempts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def wikis
|
def wikis
|
||||||
|
|
|
@ -6,6 +6,10 @@ module QA
|
||||||
class Request
|
class Request
|
||||||
API_VERSION = 'v4'
|
API_VERSION = 'v4'
|
||||||
|
|
||||||
|
def self.masked_url(url)
|
||||||
|
url.sub(/private_token=.*/, "private_token=[****]")
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(api_client, path, **query_string)
|
def initialize(api_client, path, **query_string)
|
||||||
query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token]
|
query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token]
|
||||||
request_path = request_path(path, **query_string)
|
request_path = request_path(path, **query_string)
|
||||||
|
@ -13,7 +17,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def mask_url
|
def mask_url
|
||||||
@session_address.address.sub(/private_token=.*/, "private_token=[****]")
|
QA::Runtime::API::Request.masked_url(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
|
|
|
@ -135,10 +135,12 @@ module QA
|
||||||
imported_project # import the project
|
imported_project # import the project
|
||||||
fetch_github_objects # fetch all objects right after import has started
|
fetch_github_objects # fetch all objects right after import has started
|
||||||
|
|
||||||
expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(
|
import_status = lambda do
|
||||||
duration: 3600,
|
imported_project.reload!.import_status.tap do |status|
|
||||||
interval: 30
|
raise "Import of '#{imported_project.name}' failed!" if status == 'failed'
|
||||||
)
|
end
|
||||||
|
end
|
||||||
|
expect(import_status).to eventually_eq('finished').within(duration: 3600, interval: 30)
|
||||||
@import_time = Time.now - start
|
@import_time = Time.now - start
|
||||||
|
|
||||||
aggregate_failures do
|
aggregate_failures do
|
||||||
|
@ -264,7 +266,7 @@ module QA
|
||||||
def gl_commits
|
def gl_commits
|
||||||
@gl_commits ||= begin
|
@gl_commits ||= begin
|
||||||
logger.debug("= Fetching commits =")
|
logger.debug("= Fetching commits =")
|
||||||
imported_project.commits(auto_paginate: true).map { |c| c[:id] }
|
imported_project.commits(auto_paginate: true, attempts: 2).map { |c| c[:id] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -294,7 +296,7 @@ module QA
|
||||||
def mrs
|
def mrs
|
||||||
@mrs ||= begin
|
@mrs ||= begin
|
||||||
logger.debug("= Fetching merge requests =")
|
logger.debug("= Fetching merge requests =")
|
||||||
imported_mrs = imported_project.merge_requests(auto_paginate: true)
|
imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2)
|
||||||
logger.debug("= Transforming merge request objects for comparison =")
|
logger.debug("= Transforming merge request objects for comparison =")
|
||||||
imported_mrs.each_with_object({}) do |mr, hash|
|
imported_mrs.each_with_object({}) do |mr, hash|
|
||||||
resource = Resource::MergeRequest.init do |resource|
|
resource = Resource::MergeRequest.init do |resource|
|
||||||
|
@ -305,7 +307,7 @@ module QA
|
||||||
|
|
||||||
hash[mr[:title]] = {
|
hash[mr[:title]] = {
|
||||||
body: mr[:description],
|
body: mr[:description],
|
||||||
comments: resource.comments(auto_paginate: true)
|
comments: resource.comments(auto_paginate: true, attempts: 2)
|
||||||
# remove system notes
|
# remove system notes
|
||||||
.reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) }
|
.reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) }
|
||||||
.map { |c| sanitize(c[:body]) }
|
.map { |c| sanitize(c[:body]) }
|
||||||
|
@ -320,7 +322,7 @@ module QA
|
||||||
def gl_issues
|
def gl_issues
|
||||||
@gl_issues ||= begin
|
@gl_issues ||= begin
|
||||||
logger.debug("= Fetching issues =")
|
logger.debug("= Fetching issues =")
|
||||||
imported_issues = imported_project.issues(auto_paginate: true)
|
imported_issues = imported_project.issues(auto_paginate: true, attempts: 2)
|
||||||
logger.debug("= Transforming issue objects for comparison =")
|
logger.debug("= Transforming issue objects for comparison =")
|
||||||
imported_issues.each_with_object({}) do |issue, hash|
|
imported_issues.each_with_object({}) do |issue, hash|
|
||||||
resource = Resource::Issue.init do |issue_resource|
|
resource = Resource::Issue.init do |issue_resource|
|
||||||
|
@ -331,7 +333,7 @@ module QA
|
||||||
|
|
||||||
hash[issue[:title]] = {
|
hash[issue[:title]] = {
|
||||||
body: issue[:description],
|
body: issue[:description],
|
||||||
comments: resource.comments(auto_paginate: true).map { |c| sanitize(c[:body]) }
|
comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -79,16 +79,27 @@ module QA
|
||||||
error.response
|
error.response
|
||||||
end
|
end
|
||||||
|
|
||||||
def auto_paginated_response(url)
|
def auto_paginated_response(url, attempts: 0)
|
||||||
pages = []
|
pages = []
|
||||||
with_paginated_response_body(url) { |response| pages << response }
|
with_paginated_response_body(url, attempts: attempts) { |response| pages << response }
|
||||||
|
|
||||||
pages.flatten
|
pages.flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_paginated_response_body(url)
|
def with_paginated_response_body(url, attempts: 0)
|
||||||
|
not_ok_error = lambda do |resp|
|
||||||
|
raise "Failed to GET #{QA::Runtime::API::Request.masked_url(url)} - (#{resp.code}): `#{resp}`."
|
||||||
|
end
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
response = get(url)
|
response = if attempts > 0
|
||||||
|
Retrier.retry_on_exception(max_attempts: attempts, log: false) do
|
||||||
|
get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
|
||||||
|
end
|
||||||
|
|
||||||
page, pages = response.headers.values_at(:x_page, :x_total_pages)
|
page, pages = response.headers.values_at(:x_page, :x_total_pages)
|
||||||
api_endpoint = url.match(%r{v4/(\S+)\?})[1]
|
api_endpoint = url.match(%r{v4/(\S+)\?})[1]
|
||||||
|
|
||||||
|
@ -104,7 +115,10 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_links(response)
|
def pagination_links(response)
|
||||||
response.headers[:link].split(',').map do |link|
|
link = response.headers[:link]
|
||||||
|
return unless link
|
||||||
|
|
||||||
|
link.split(',').map do |link|
|
||||||
match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
|
match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
|
||||||
break nil unless match
|
break nil unless match
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,15 @@ module QA
|
||||||
RetriesExceededError = Class.new(RepeaterConditionExceededError)
|
RetriesExceededError = Class.new(RepeaterConditionExceededError)
|
||||||
WaitExceededError = Class.new(RepeaterConditionExceededError)
|
WaitExceededError = Class.new(RepeaterConditionExceededError)
|
||||||
|
|
||||||
def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true)
|
def repeat_until(
|
||||||
|
max_attempts: nil,
|
||||||
|
max_duration: nil,
|
||||||
|
reload_page: nil,
|
||||||
|
sleep_interval: 0,
|
||||||
|
raise_on_failure: true,
|
||||||
|
retry_on_exception: false,
|
||||||
|
log: true
|
||||||
|
)
|
||||||
attempts = 0
|
attempts = 0
|
||||||
start = Time.now
|
start = Time.now
|
||||||
|
|
||||||
|
@ -29,17 +37,19 @@ module QA
|
||||||
raise unless retry_on_exception
|
raise unless retry_on_exception
|
||||||
|
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
|
raise unless remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
|
||||||
sleep_and_reload_if_needed(sleep_interval, reload_page)
|
|
||||||
|
|
||||||
retry
|
sleep_and_reload_if_needed(sleep_interval, reload_page)
|
||||||
else
|
retry
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if raise_on_failure
|
if raise_on_failure
|
||||||
raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts)
|
unless remaining_attempts?(attempts, max_attempts)
|
||||||
|
raise(
|
||||||
|
RetriesExceededError,
|
||||||
|
"Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
|
raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,21 +7,21 @@ module QA
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5)
|
def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true)
|
||||||
QA::Runtime::Logger.debug(
|
if log
|
||||||
<<~MSG.tr("\n", ' ')
|
msg = ["with retry_on_exception: max_attempts: #{max_attempts}"]
|
||||||
with retry_on_exception: max_attempts: #{max_attempts};
|
msg << "reload_page: #{reload_page}" if reload_page
|
||||||
reload_page: #{reload_page};
|
msg << "sleep_interval: #{sleep_interval}"
|
||||||
sleep_interval: #{sleep_interval}
|
QA::Runtime::Logger.debug(msg.join('; '))
|
||||||
MSG
|
end
|
||||||
)
|
|
||||||
|
|
||||||
result = nil
|
result = nil
|
||||||
repeat_until(
|
repeat_until(
|
||||||
max_attempts: max_attempts,
|
max_attempts: max_attempts,
|
||||||
reload_page: reload_page,
|
reload_page: reload_page,
|
||||||
sleep_interval: sleep_interval,
|
sleep_interval: sleep_interval,
|
||||||
retry_on_exception: true
|
retry_on_exception: true,
|
||||||
|
log: log
|
||||||
) do
|
) do
|
||||||
result = yield
|
result = yield
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ module QA
|
||||||
# We set it to `true` so that it doesn't repeat if there's no exception
|
# We set it to `true` so that it doesn't repeat if there's no exception
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
QA::Runtime::Logger.debug("ended retry_on_exception")
|
QA::Runtime::Logger.debug("ended retry_on_exception") if log
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,8 +70,9 @@ RSpec.describe QA::Support::Retrier do
|
||||||
describe '.retry_on_exception' do
|
describe '.retry_on_exception' do
|
||||||
context 'when the condition is true' do
|
context 'when the condition is true' do
|
||||||
it 'logs max_attempts, reload_page, and sleep_interval parameters' do
|
it 'logs max_attempts, reload_page, and sleep_interval parameters' do
|
||||||
expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } }
|
message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
|
||||||
.to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process
|
expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { true } }
|
||||||
|
.to output(message).to_stdout_from_any_process
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs the end' do
|
it 'logs the end' do
|
||||||
|
@ -82,8 +83,9 @@ RSpec.describe QA::Support::Retrier do
|
||||||
|
|
||||||
context 'when the condition is false' do
|
context 'when the condition is false' do
|
||||||
it 'logs the start' do
|
it 'logs the start' do
|
||||||
expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } }
|
message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
|
||||||
.to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process
|
expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { false } }
|
||||||
|
.to output(message).to_stdout_from_any_process
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs the end' do
|
it 'logs the end' do
|
||||||
|
|
|
@ -652,7 +652,7 @@ RSpec.describe Projects::FeatureFlagsController do
|
||||||
version: 'new_version_flag',
|
version: 'new_version_flag',
|
||||||
strategies_attributes: [{
|
strategies_attributes: [{
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '15', stickiness: 'DEFAULT' },
|
parameters: { groupId: 'default', rollout: '15', stickiness: 'default' },
|
||||||
scopes_attributes: [{ environment_scope: 'production' }]
|
scopes_attributes: [{ environment_scope: 'production' }]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -666,7 +666,7 @@ RSpec.describe Projects::FeatureFlagsController do
|
||||||
|
|
||||||
strategy_json = json_response['strategies'].first
|
strategy_json = json_response['strategies'].first
|
||||||
expect(strategy_json['name']).to eq('flexibleRollout')
|
expect(strategy_json['name']).to eq('flexibleRollout')
|
||||||
expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'DEFAULT' })
|
expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'default' })
|
||||||
expect(strategy_json['scopes'].count).to eq(1)
|
expect(strategy_json['scopes'].count).to eq(1)
|
||||||
|
|
||||||
scope_json = strategy_json['scopes'].first
|
scope_json = strategy_json['scopes'].first
|
||||||
|
@ -938,7 +938,7 @@ RSpec.describe Projects::FeatureFlagsController do
|
||||||
it 'creates a flexibleRollout strategy' do
|
it 'creates a flexibleRollout strategy' do
|
||||||
put_request(new_version_flag, strategies_attributes: [{
|
put_request(new_version_flag, strategies_attributes: [{
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '30', stickiness: 'DEFAULT' }
|
parameters: { groupId: 'default', rollout: '30', stickiness: 'default' }
|
||||||
}])
|
}])
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
@ -948,7 +948,7 @@ RSpec.describe Projects::FeatureFlagsController do
|
||||||
expect(strategy_json['parameters']).to eq({
|
expect(strategy_json['parameters']).to eq({
|
||||||
'groupId' => 'default',
|
'groupId' => 'default',
|
||||||
'rollout' => '30',
|
'rollout' => '30',
|
||||||
'stickiness' => 'DEFAULT'
|
'stickiness' => 'default'
|
||||||
})
|
})
|
||||||
expect(strategy_json['scopes']).to eq([])
|
expect(strategy_json['scopes']).to eq([])
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,7 +51,7 @@ FactoryBot.define do
|
||||||
ci_job_token_scope_enabled { nil }
|
ci_job_token_scope_enabled { nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
before(:create) do |project, evaluator|
|
after(:build) do |project, evaluator|
|
||||||
# Builds and MRs can't have higher visibility level than repository access level.
|
# Builds and MRs can't have higher visibility level than repository access level.
|
||||||
builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
|
builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
|
||||||
merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
|
merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
|
||||||
|
@ -67,21 +67,11 @@ FactoryBot.define do
|
||||||
pages_access_level: evaluator.pages_access_level,
|
pages_access_level: evaluator.pages_access_level,
|
||||||
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level,
|
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level,
|
||||||
operations_access_level: evaluator.operations_access_level,
|
operations_access_level: evaluator.operations_access_level,
|
||||||
analytics_access_level: evaluator.analytics_access_level
|
analytics_access_level: evaluator.analytics_access_level,
|
||||||
|
container_registry_access_level: evaluator.container_registry_access_level
|
||||||
}
|
}
|
||||||
|
|
||||||
project.build_project_feature(hash)
|
project.build_project_feature(hash)
|
||||||
|
|
||||||
# This is not included in the `hash` above because the default_value_for in
|
|
||||||
# the ProjectFeature model overrides the value set by `build_project_feature` when
|
|
||||||
# evaluator.container_registry_access_level == ProjectFeature::DISABLED.
|
|
||||||
#
|
|
||||||
# This is because the default_value_for gem uses the <column>_changed? method
|
|
||||||
# to determine if the default value should be applied. For new records,
|
|
||||||
# <column>_changed? returns false if the value of the column is the same as
|
|
||||||
# the database default.
|
|
||||||
# See https://github.com/FooBarWidget/default_value_for/blob/release-3.4.0/lib/default_value_for.rb#L158.
|
|
||||||
project.project_feature.container_registry_access_level = evaluator.container_registry_access_level
|
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:create) do |project, evaluator|
|
after(:create) do |project, evaluator|
|
||||||
|
|
|
@ -66,15 +66,14 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits a change when the stickiness value changes', async () => {
|
it('emits a change when the stickiness value changes', async () => {
|
||||||
stickinessSelect.setValue('USERID');
|
await stickinessSelect.setValue('userId');
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
expect(wrapper.emitted('change')).toEqual([
|
expect(wrapper.emitted('change')).toEqual([
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
parameters: {
|
parameters: {
|
||||||
rollout: flexibleRolloutStrategy.parameters.rollout,
|
rollout: flexibleRolloutStrategy.parameters.rollout,
|
||||||
groupId: PERCENT_ROLLOUT_GROUP_ID,
|
groupId: PERCENT_ROLLOUT_GROUP_ID,
|
||||||
stickiness: 'USERID',
|
stickiness: 'userId',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -76,7 +76,7 @@ export const percentRolloutStrategy = {
|
||||||
|
|
||||||
export const flexibleRolloutStrategy = {
|
export const flexibleRolloutStrategy = {
|
||||||
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
|
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
|
||||||
parameters: { rollout: '50', groupId: 'default', stickiness: 'DEFAULT' },
|
parameters: { rollout: '50', groupId: 'default', stickiness: 'default' },
|
||||||
scopes: [],
|
scopes: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
require_migration!('operations_feature_flags_correct_flexible_rollout_values')
|
||||||
|
|
||||||
|
RSpec.describe OperationsFeatureFlagsCorrectFlexibleRolloutValues, :migration do
|
||||||
|
let_it_be(:strategies) { table(:operations_strategies) }
|
||||||
|
|
||||||
|
let(:namespace) { table(:namespaces).create!(name: 'feature_flag', path: 'feature_flag') }
|
||||||
|
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
|
||||||
|
let(:feature_flag) { table(:operations_feature_flags).create!(project_id: project.id, active: true, name: 'foo', iid: 1) }
|
||||||
|
|
||||||
|
describe "#up" do
|
||||||
|
described_class::STICKINESS.each do |old, new|
|
||||||
|
it "corrects parameters for flexible rollout stickiness #{old}" do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
parameters = { groupId: "default", rollout: "100", stickiness: old }
|
||||||
|
strategy = create_strategy(parameters)
|
||||||
|
|
||||||
|
migration.before -> {
|
||||||
|
expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => old })
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => new })
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores other strategies' do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "USERID" }
|
||||||
|
strategy = create_strategy(parameters, name: 'default')
|
||||||
|
|
||||||
|
migration.before -> {
|
||||||
|
expect(strategy.reload.parameters).to eq(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(strategy.reload.parameters).to eq(parameters)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores other stickiness' do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "FOO" }
|
||||||
|
strategy = create_strategy(parameters)
|
||||||
|
|
||||||
|
migration.before -> {
|
||||||
|
expect(strategy.reload.parameters).to eq(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(strategy.reload.parameters).to eq(parameters)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_strategy(params, name: 'flexibleRollout')
|
||||||
|
strategies.create!(name: name, parameters: params, feature_flag_id: feature_flag.id)
|
||||||
|
end
|
||||||
|
end
|
|
@ -208,23 +208,9 @@ RSpec.describe Namespace do
|
||||||
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
|
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
|
||||||
it { is_expected.to include_module(Namespaces::Traversal::Recursive) }
|
it { is_expected.to include_module(Namespaces::Traversal::Recursive) }
|
||||||
it { is_expected.to include_module(Namespaces::Traversal::Linear) }
|
it { is_expected.to include_module(Namespaces::Traversal::Linear) }
|
||||||
it { is_expected.to include_module(Namespaces::Traversal::RecursiveScopes) }
|
|
||||||
it { is_expected.to include_module(Namespaces::Traversal::LinearScopes) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'traversal scopes' do
|
it_behaves_like 'linear namespace traversal'
|
||||||
context 'recursive' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(use_traversal_ids: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'namespace traversal scopes'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'linear' do
|
|
||||||
it_behaves_like 'namespace traversal scopes'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'traversal_ids on create' do
|
context 'traversal_ids on create' do
|
||||||
context 'default traversal_ids' do
|
context 'default traversal_ids' do
|
||||||
|
|
|
@ -112,7 +112,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the strategy name is flexibleRollout' do
|
context 'when the strategy name is flexibleRollout' do
|
||||||
valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'DEFAULT' }
|
valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'default' }
|
||||||
where(invalid_parameters: [
|
where(invalid_parameters: [
|
||||||
nil,
|
nil,
|
||||||
{},
|
{},
|
||||||
|
@ -133,7 +133,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
|
|
||||||
[
|
[
|
||||||
[:rollout, '10'],
|
[:rollout, '10'],
|
||||||
[:stickiness, 'DEFAULT'],
|
[:stickiness, 'default'],
|
||||||
[:groupId, 'mygroup']
|
[:groupId, 'mygroup']
|
||||||
].permutation(3).each do |parameters|
|
].permutation(3).each do |parameters|
|
||||||
it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do
|
it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do
|
||||||
|
@ -151,7 +151,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
"\n", "\t", "\n10", "20\n", "\n100", "100\n", "\n ", nil])
|
"\n", "\t", "\n10", "20\n", "\n100", "100\n", "\n ", nil])
|
||||||
with_them do
|
with_them do
|
||||||
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
||||||
parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: invalid_value }
|
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: invalid_value }
|
||||||
strategy = described_class.create(feature_flag: feature_flag,
|
strategy = described_class.create(feature_flag: feature_flag,
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: parameters)
|
parameters: parameters)
|
||||||
|
@ -165,7 +165,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
where(valid_value: %w[0 1 10 38 100 93])
|
where(valid_value: %w[0 1 10 38 100 93])
|
||||||
with_them do
|
with_them do
|
||||||
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
||||||
parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: valid_value }
|
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: valid_value }
|
||||||
strategy = described_class.create(feature_flag: feature_flag,
|
strategy = described_class.create(feature_flag: feature_flag,
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: parameters)
|
parameters: parameters)
|
||||||
|
@ -180,7 +180,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
'!bad', '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"])
|
'!bad', '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"])
|
||||||
with_them do
|
with_them do
|
||||||
it 'must be a string value of up to 32 lowercase characters' do
|
it 'must be a string value of up to 32 lowercase characters' do
|
||||||
parameters = { stickiness: 'DEFAULT', groupId: invalid_value, rollout: '40' }
|
parameters = { stickiness: 'default', groupId: invalid_value, rollout: '40' }
|
||||||
strategy = described_class.create(feature_flag: feature_flag,
|
strategy = described_class.create(feature_flag: feature_flag,
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: parameters)
|
parameters: parameters)
|
||||||
|
@ -192,7 +192,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
where(valid_value: ["somegroup", "anothergroup", "okay", "g", "a" * 32])
|
where(valid_value: ["somegroup", "anothergroup", "okay", "g", "a" * 32])
|
||||||
with_them do
|
with_them do
|
||||||
it 'must be a string value of up to 32 lowercase characters' do
|
it 'must be a string value of up to 32 lowercase characters' do
|
||||||
parameters = { stickiness: 'DEFAULT', groupId: valid_value, rollout: '40' }
|
parameters = { stickiness: 'default', groupId: valid_value, rollout: '40' }
|
||||||
strategy = described_class.create(feature_flag: feature_flag,
|
strategy = described_class.create(feature_flag: feature_flag,
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: parameters)
|
parameters: parameters)
|
||||||
|
@ -203,7 +203,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'stickiness' do
|
describe 'stickiness' do
|
||||||
where(invalid_value: [nil, " ", "default", "DEFAULT\n", "UserId", "USER", "USERID "])
|
where(invalid_value: [nil, " ", "DEFAULT", "DEFAULT\n", "UserId", "USER", "USERID "])
|
||||||
with_them do
|
with_them do
|
||||||
it 'must be a string representing a supported stickiness setting' do
|
it 'must be a string representing a supported stickiness setting' do
|
||||||
parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' }
|
parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' }
|
||||||
|
@ -212,12 +212,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
parameters: parameters)
|
parameters: parameters)
|
||||||
|
|
||||||
expect(strategy.errors[:parameters]).to eq([
|
expect(strategy.errors[:parameters]).to eq([
|
||||||
'stickiness parameter must be DEFAULT, USERID, SESSIONID, or RANDOM'
|
'stickiness parameter must be default, userId, sessionId, or random'
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
where(valid_value: %w[DEFAULT USERID SESSIONID RANDOM])
|
where(valid_value: %w[default userId sessionId random])
|
||||||
with_them do
|
with_them do
|
||||||
it 'must be a string representing a supported stickiness setting' do
|
it 'must be a string representing a supported stickiness setting' do
|
||||||
parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' }
|
parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' }
|
||||||
|
@ -425,7 +425,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
user_list: user_list,
|
user_list: user_list,
|
||||||
parameters: { groupId: 'default',
|
parameters: { groupId: 'default',
|
||||||
rollout: '10',
|
rollout: '10',
|
||||||
stickiness: 'DEFAULT' })
|
stickiness: 'default' })
|
||||||
|
|
||||||
expect(strategy.errors[:user_list]).to eq(['must be blank'])
|
expect(strategy.errors[:user_list]).to eq(['must be blank'])
|
||||||
end
|
end
|
||||||
|
@ -435,7 +435,7 @@ RSpec.describe Operations::FeatureFlags::Strategy do
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default',
|
parameters: { groupId: 'default',
|
||||||
rollout: '10',
|
rollout: '10',
|
||||||
stickiness: 'DEFAULT' })
|
stickiness: 'default' })
|
||||||
|
|
||||||
expect(strategy.errors[:user_list]).to be_empty
|
expect(strategy.errors[:user_list]).to be_empty
|
||||||
end
|
end
|
||||||
|
|
|
@ -218,5 +218,29 @@ RSpec.describe ProjectFeature do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'test build factory' do
|
||||||
|
let(:project) { build(:project, container_registry_access_level: level) }
|
||||||
|
|
||||||
|
subject { project.container_registry_access_level }
|
||||||
|
|
||||||
|
context 'private' do
|
||||||
|
let(:level) { ProjectFeature::PRIVATE }
|
||||||
|
|
||||||
|
it { is_expected.to eq(level) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'enabled' do
|
||||||
|
let(:level) { ProjectFeature::ENABLED }
|
||||||
|
|
||||||
|
it { is_expected.to eq(level) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'disabled' do
|
||||||
|
let(:level) { ProjectFeature::DISABLED }
|
||||||
|
|
||||||
|
it { is_expected.to eq(level) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -318,7 +318,8 @@ RSpec.describe Project, factory_default: :keep do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'validates presence of project_feature' do
|
it 'validates presence of project_feature' do
|
||||||
project = build(:project, project_feature: nil)
|
project = build(:project)
|
||||||
|
project.project_feature = nil
|
||||||
|
|
||||||
expect(project).not_to be_valid
|
expect(project).not_to be_valid
|
||||||
end
|
end
|
||||||
|
|
|
@ -417,7 +417,7 @@ RSpec.describe API::FeatureFlags do
|
||||||
version: 'new_version_flag',
|
version: 'new_version_flag',
|
||||||
strategies: [{
|
strategies: [{
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '50', stickiness: 'DEFAULT' },
|
parameters: { groupId: 'default', rollout: '50', stickiness: 'default' },
|
||||||
scopes: [{
|
scopes: [{
|
||||||
environment_scope: 'staging'
|
environment_scope: 'staging'
|
||||||
}]
|
}]
|
||||||
|
@ -434,7 +434,7 @@ RSpec.describe API::FeatureFlags do
|
||||||
expect(feature_flag.version).to eq('new_version_flag')
|
expect(feature_flag.version).to eq('new_version_flag')
|
||||||
expect(feature_flag.strategies.map { |s| s.slice(:name, :parameters).deep_symbolize_keys }).to eq([{
|
expect(feature_flag.strategies.map { |s| s.slice(:name, :parameters).deep_symbolize_keys }).to eq([{
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '50', stickiness: 'DEFAULT' }
|
parameters: { groupId: 'default', rollout: '50', stickiness: 'default' }
|
||||||
}])
|
}])
|
||||||
expect(feature_flag.strategies.first.scopes.map { |s| s.slice(:environment_scope).deep_symbolize_keys }).to eq([{
|
expect(feature_flag.strategies.first.scopes.map { |s| s.slice(:environment_scope).deep_symbolize_keys }).to eq([{
|
||||||
environment_scope: 'staging'
|
environment_scope: 'staging'
|
||||||
|
@ -630,7 +630,7 @@ RSpec.describe API::FeatureFlags do
|
||||||
strategies: [{
|
strategies: [{
|
||||||
id: strategy.id,
|
id: strategy.id,
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
|
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +642,7 @@ RSpec.describe API::FeatureFlags do
|
||||||
expect(result).to eq([{
|
expect(result).to eq([{
|
||||||
id: strategy.id,
|
id: strategy.id,
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
|
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
|
||||||
}])
|
}])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -677,7 +677,7 @@ RSpec.describe API::FeatureFlags do
|
||||||
params = {
|
params = {
|
||||||
strategies: [{
|
strategies: [{
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
|
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -694,7 +694,7 @@ RSpec.describe API::FeatureFlags do
|
||||||
parameters: {}
|
parameters: {}
|
||||||
}, {
|
}, {
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { groupId: 'default', rollout: '10', stickiness: 'DEFAULT' }
|
parameters: { groupId: 'default', rollout: '10', stickiness: 'default' }
|
||||||
}])
|
}])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Traversal examples common to linear and recursive methods are in
|
||||||
|
# spec/support/shared_examples/namespaces/traversal_examples.rb
|
||||||
|
|
||||||
|
RSpec.shared_examples 'linear namespace traversal' do
|
||||||
|
context 'when use_traversal_ids feature flag is enabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(use_traversal_ids: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'scopes' do
|
||||||
|
describe '.as_ids' do
|
||||||
|
let_it_be(:namespace1) { create(:group) }
|
||||||
|
let_it_be(:namespace2) { create(:group) }
|
||||||
|
|
||||||
|
subject { Namespace.where(id: [namespace1, namespace2]).as_ids.pluck(:id) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(namespace1.id, namespace2.id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,51 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.shared_examples 'namespace traversal scopes' do
|
|
||||||
# Hierarchy 1
|
|
||||||
let_it_be(:group_1) { create(:group) }
|
|
||||||
let_it_be(:nested_group_1) { create(:group, parent: group_1) }
|
|
||||||
let_it_be(:deep_nested_group_1) { create(:group, parent: nested_group_1) }
|
|
||||||
|
|
||||||
# Hierarchy 2
|
|
||||||
let_it_be(:group_2) { create(:group) }
|
|
||||||
let_it_be(:nested_group_2) { create(:group, parent: group_2) }
|
|
||||||
let_it_be(:deep_nested_group_2) { create(:group, parent: nested_group_2) }
|
|
||||||
|
|
||||||
# All groups
|
|
||||||
let_it_be(:groups) do
|
|
||||||
[
|
|
||||||
group_1, nested_group_1, deep_nested_group_1,
|
|
||||||
group_2, nested_group_2, deep_nested_group_2
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.as_ids' do
|
|
||||||
subject { described_class.where(id: [group_1, group_2]).as_ids.pluck(:id) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(group_1.id, group_2.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.without_sti_condition' do
|
|
||||||
subject { described_class.without_sti_condition }
|
|
||||||
|
|
||||||
it { expect(subject.where_values_hash).not_to have_key(:type) }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.self_and_descendants' do
|
|
||||||
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1, nested_group_2, deep_nested_group_2) }
|
|
||||||
|
|
||||||
context 'with duplicate descendants' do
|
|
||||||
subject { described_class.where(id: [group_1, group_2, nested_group_1]).self_and_descendants }
|
|
||||||
|
|
||||||
it { is_expected.to match_array(groups) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.self_and_descendant_ids' do
|
|
||||||
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendant_ids.pluck(:id) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(nested_group_1.id, deep_nested_group_1.id, nested_group_2.id, deep_nested_group_2.id) }
|
|
||||||
end
|
|
||||||
end
|
|
24
yarn.lock
24
yarn.lock
|
@ -3418,12 +3418,12 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0:
|
commander@2, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@~2.20.0:
|
||||||
version "2.20.0"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
|
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||||
|
|
||||||
commander@^6.0.0, commander@^6.2.0, commander@~6.2.1:
|
commander@^6.0.0, commander@^6.1.0, commander@^6.2.0, commander@~6.2.1:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||||
|
@ -10560,7 +10560,7 @@ semver-diff@^3.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
|
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
|
||||||
version "5.7.1"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
@ -12882,14 +12882,14 @@ yarn-check-webpack-plugin@^1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^2.4.2"
|
chalk "^2.4.2"
|
||||||
|
|
||||||
yarn-deduplicate@^1.1.1:
|
yarn-deduplicate@^3.1.0:
|
||||||
version "1.1.1"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.1.tgz#19b4a87654b66f55bf3a4bd6b153b4e4ab1b6e6d"
|
resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-3.1.0.tgz#3018d93e95f855f236a215b591fe8bc4bcabba3e"
|
||||||
integrity sha512-2FDJ1dFmtvqhRmfja89ohYzpaheCYg7BFBSyaUq+kxK0y61C9oHv1XaQovCWGJtP2WU8PksQOgzMVV7oQOobzw==
|
integrity sha512-q2VZ6ThNzQpGfNpkPrkmV7x5HT9MOhCUsTxVTzyyZB0eSXz1NTodHn+r29DlLb+peKk8iXxzdUVhQG9pI7moFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@yarnpkg/lockfile" "^1.1.0"
|
"@yarnpkg/lockfile" "^1.1.0"
|
||||||
commander "^2.10.0"
|
commander "^6.1.0"
|
||||||
semver "^5.3.0"
|
semver "^7.3.2"
|
||||||
|
|
||||||
yeast@0.1.2:
|
yeast@0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
|
|
Loading…
Reference in New Issue