Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md20
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/finders/packages/terraform_module/packages_finder.rb32
-rw-r--r--app/graphql/types/namespace_type.rb2
-rw-r--r--app/models/work_item.rb2
-rw-r--r--app/models/work_items/hierarchy_restriction.rb9
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb18
-rwxr-xr-xbin/feature-flag243
-rw-r--r--db/docs/batched_background_migrations/update_workspaces_config_version.yml3
-rw-r--r--db/migrate/20231130124606_add_project_id_name_version_id_index_to_installable_terraform_modules.rb23
-rw-r--r--db/post_migrate/20231215151348_finalize_workspaces_config_version_2_migration.rb23
-rw-r--r--db/schema_migrations/202311301246061
-rw-r--r--db/schema_migrations/202312151513481
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/settings/jira_cloud_app_troubleshooting.md6
-rw-r--r--doc/api/graphql/reference/index.md12
-rw-r--r--doc/api/packages/terraform-modules.md25
-rw-r--r--doc/development/feature_flags/index.md167
-rw-r--r--doc/development/work_items.md3
-rw-r--r--doc/user/packages/terraform_module_registry/index.md28
-rw-r--r--doc/user/project/pages/pages_access_control.md9
-rw-r--r--lib/api/terraform/modules/v1/project_packages.rb215
-rw-r--r--lib/feature/definition.rb16
-rw-r--r--lib/feature/shared.rb63
-rw-r--r--lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb5
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb8
-rw-r--r--lib/gitlab/usage_data_counters/quick_action_activity_unique_counter.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/bin/feature_flag_spec.rb273
-rw-r--r--spec/finders/packages/terraform_module/packages_finder_spec.rb65
-rw-r--r--spec/graphql/types/namespace_type_spec.rb2
-rw-r--r--spec/lib/feature/definition_spec.rb6
-rw-r--r--spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb8
-rw-r--r--spec/models/work_items/hierarchy_restriction_spec.rb20
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb21
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace_query_spec.rb48
-rw-r--r--spec/requests/api/terraform/modules/v1/project_packages_spec.rb12
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb24
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb7
-rw-r--r--spec/support/formatters/json_formatter.rb8
-rw-r--r--spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb175
-rw-r--r--spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb17
43 files changed, 1269 insertions, 365 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
index 19796eda71a..5ae90aa4671 100644
--- a/.gitlab/issue_templates/Feature Flag Roll Out.md
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -1,16 +1,14 @@
<!-- Title suggestion: [Feature flag] Enable <feature-flag-name> -->
-[main-issue]: MAIN-ISSUE-LINK
-
## Summary
-This issue is to roll out [the feature][main-issue] on production,
+This issue is to roll out [the feature](<feature-issue-link>) on production,
that is currently behind the `<feature-flag-name>` feature flag.
## Owners
-- Most appropriate Slack channel to reach out to: `#g_TEAM_NAME`
-- Best individual to reach out to: GITLAB_USERNAME_OF_DRI
+- Most appropriate Slack channel to reach out to: `#<slack-channel-of-dri-team>`
+- Best individual to reach out to: @<gitlab-username-of-dri>
## Expectations
@@ -52,7 +50,7 @@ and cross-posted (with the command results) to the responsible team's Slack chan
- [ ] Depending on the [type of actor](https://docs.gitlab.com/ee/development/feature_flags/#feature-actors) you are using, pick one of these options:
- For **project-actor**: `/chatops run feature set --project=gitlab-org/gitlab,gitlab-org/gitlab-foss,gitlab-com/www-gitlab-com <feature-flag-name> true`
- For **group-actor**: `/chatops run feature set --group=gitlab-org,gitlab-com <feature-flag-name> true`
- - For **user-actor**: `/chatops run feature set --user=<your-username> <feature-flag-name> true`
+ - For **user-actor**: `/chatops run feature set --user=<gitlab-username-of-dri> <feature-flag-name> true`
- [ ] Verify that the feature works for the specific actors.
### Preparation before global rollout
@@ -64,7 +62,7 @@ and cross-posted (with the command results) to the responsible team's Slack chan
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production.
If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
- [ ] Ensure that documentation exists for the feature, and the [version history text](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#add-version-history-text) has been updated.
-- [ ] Leave a comment on [the feature issue][main-issue] announcing estimated time when this feature flag will be enabled on GitLab.com.
+- [ ] Leave a comment on [the feature issue](<feature-issue-link>) announcing estimated time when this feature flag will be enabled on GitLab.com.
- [ ] Ensure that any breaking changes have been announced following the [release post process](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes) to ensure GitLab customers are aware.
- [ ] Notify the [`#support_gitlab-com` Slack channel](https://gitlab.slack.com/archives/C4XFU81LG) and your team channel ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)).
- [ ] Ensure that the feature flag rollout plan is reviewed by another developer familiar with the domain.
@@ -72,7 +70,7 @@ and cross-posted (with the command results) to the responsible team's Slack chan
### Global rollout on production
For visibility, all `/chatops` commands that target production should be executed in the [`#production` Slack channel](https://gitlab.slack.com/archives/C101F3796)
-and cross-posted (with the command results) to the responsible team's Slack channel (`#g_TEAM_NAME`).
+and cross-posted (with the command results) to the responsible team's Slack channel (`#<slack-channel-of-dri-team>`).
- [ ] (Optional) [Incrementally roll out](https://docs.gitlab.com/ee/development/feature_flags/controls.html#process) the feature on production environment.
- Between every step wait for at least 15 minutes and monitor the appropriate graphs on https://dashboards.gitlab.net.
@@ -143,7 +141,7 @@ You can either [create a follow-up issue for Feature Flag Cleanup](https://gitla
/chatops run feature set <feature-flag-name> false
```
-/label ~group::
-/label ~"feature flag"
-/assign me
+/label <group-label>
+/label ~"feature flag" ~C4
+/assign @<gitlab-username-of-dri>
/due in 2 weeks
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 7ec13c3d54c..4ef53c673f6 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -1,6 +1,4 @@
html {
- overflow-y: scroll;
-
&.touch .tooltip {
display: none !important;
}
diff --git a/app/finders/packages/terraform_module/packages_finder.rb b/app/finders/packages/terraform_module/packages_finder.rb
new file mode 100644
index 00000000000..bcef8738622
--- /dev/null
+++ b/app/finders/packages/terraform_module/packages_finder.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Packages
+ module TerraformModule
+ class PackagesFinder
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ return ::Packages::Package.none unless project && params[:package_name]
+
+ packages
+ end
+
+ private
+
+ attr_reader :project, :params
+
+ def packages
+ result = project
+ .packages
+ .with_name(params[:package_name])
+ .terraform_module
+ .installable
+
+ params[:package_version] ? result.with_version(params[:package_version]) : result.has_version.order_version_desc
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 85bda507ff7..3420f16213f 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -4,7 +4,7 @@ module Types
class NamespaceType < BaseObject
graphql_name 'Namespace'
- authorize :read_namespace_via_membership
+ authorize :read_namespace
field :id, GraphQL::Types::ID, null: false,
description: 'ID of the namespace.'
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 77f684e3578..f1d007e8167 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -5,7 +5,7 @@ class WorkItem < Issue
COMMON_QUICK_ACTIONS_COMMANDS = [
:title, :reopen, :close, :cc, :tableflip, :shrug, :type, :promote_to, :checkin_reminder,
- :subscribe, :unsubscribe, :confidential, :award
+ :subscribe, :unsubscribe, :confidential, :award, :react
].freeze
self.table_name = 'issues'
diff --git a/app/models/work_items/hierarchy_restriction.rb b/app/models/work_items/hierarchy_restriction.rb
index a253447a8db..f74f2f037b1 100644
--- a/app/models/work_items/hierarchy_restriction.rb
+++ b/app/models/work_items/hierarchy_restriction.rb
@@ -7,8 +7,17 @@ module WorkItems
belongs_to :parent_type, class_name: 'WorkItems::Type'
belongs_to :child_type, class_name: 'WorkItems::Type'
+ after_destroy :clear_parent_type_cache!
+ after_save :clear_parent_type_cache!
+
validates :parent_type, presence: true
validates :child_type, presence: true
validates :child_type, uniqueness: { scope: :parent_type_id }
+
+ private
+
+ def clear_parent_type_cache!
+ parent_type.clear_reactive_cache!
+ end
end
end
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index c11b019cee5..1733021cbb5 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -10,9 +10,6 @@ module PagesDomains
# no particular SLA, usually takes 10-15 seconds
CERTIFICATE_PROCESSING_DELAY = 1.minute.freeze
- # Maximum domain length for Let's Encrypt
- MAX_DOMAIN_LENGTH = 64
-
attr_reader :pages_domain
def initialize(pages_domain)
@@ -20,11 +17,6 @@ module PagesDomains
end
def execute
- if pages_domain.domain.bytesize > MAX_DOMAIN_LENGTH
- log_domain_length_error
- return
- end
-
pages_domain.acme_orders.expired.delete_all
acme_order = pages_domain.acme_orders.first
@@ -67,16 +59,6 @@ module PagesDomains
NotificationService.new.pages_domain_auto_ssl_failed(pages_domain)
end
- def log_domain_length_error
- Gitlab::AppLogger.error(
- message: "Domain name too long for Let's Encrypt certificate",
- pages_domain: pages_domain.domain,
- pages_domain_bytesize: pages_domain.domain.bytesize,
- max_allowed_bytesize: MAX_DOMAIN_LENGTH,
- project_id: pages_domain.project_id
- )
- end
-
def log_error(api_order)
Gitlab::AppLogger.error(
message: "Failed to obtain Let's Encrypt certificate",
diff --git a/bin/feature-flag b/bin/feature-flag
index 415adfad9a0..a82abdf43cd 100755
--- a/bin/feature-flag
+++ b/bin/feature-flag
@@ -5,13 +5,17 @@
# Automatically stages the file and amends the previous commit if the `--amend`
# argument is used.
-require 'optparse'
-require 'yaml'
require 'fileutils'
-require 'uri'
+require 'httparty'
+require 'json'
+require 'optparse'
require 'readline'
+require 'shellwords'
+require 'uri'
+require 'yaml'
require_relative '../lib/feature/shared' unless defined?(Feature::Shared)
+require_relative '../lib/gitlab/popen'
module FeatureFlagHelpers
Abort = Class.new(StandardError)
@@ -32,6 +36,20 @@ class FeatureFlagOptionParser
extend FeatureFlagHelpers
extend ::Feature::Shared
+ WWW_GITLAB_COM_SITE = 'https://about.gitlab.com'
+ WWW_GITLAB_COM_GROUPS_JSON = "#{WWW_GITLAB_COM_SITE}/groups.json".freeze
+ FF_ROLLOUT_ISSUE_TEMPLATE = '.gitlab/issue_templates/Feature Flag Roll Out.md'
+ COPY_COMMANDS = [
+ 'pbcopy', # macOS
+ 'xclip -selection clipboard', # Linux
+ 'xsel --clipboard --input', # Linux
+ 'wl-copy' # Wayland
+ ].freeze
+ OPEN_COMMANDS = [
+ 'open', # macOS
+ 'xdg-open' # Linux
+ ].freeze
+
Options = Struct.new(
:name,
:type,
@@ -41,8 +59,11 @@ class FeatureFlagOptionParser
:amend,
:dry_run,
:force,
+ :feature_issue_url,
:introduced_by_url,
- :rollout_issue_url
+ :rollout_issue_url,
+ :username,
+ keyword_init: true
)
class << self
@@ -62,6 +83,10 @@ class FeatureFlagOptionParser
options.force = value
end
+ opts.on('-a', '--feature-issue-url [string]', String, 'URL of the original feature issue') do |value|
+ options.feature_issue_url = value
+ end
+
opts.on('-m', '--introduced-by-url [string]', String, 'URL of merge request introducing the Feature Flag') do |value|
options.introduced_by_url = value
end
@@ -79,11 +104,15 @@ class FeatureFlagOptionParser
end
opts.on('-g', '--group [string]', String, "The group introducing a feature flag, like: `group::project management`") do |value|
- options.group = value if value.start_with?('group::')
+ options.group = value if group_labels.include?(value)
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]
+ options.type = value.to_sym if TYPES.key?(value.to_sym)
+ end
+
+ opts.on('-u', '--username [string]', String, "The username of the feature flag DRI") do |value|
+ options.username = value
end
opts.on('-e', '--ee', 'Generate a feature flag entry for GitLab EE') do |value|
@@ -110,82 +139,133 @@ class FeatureFlagOptionParser
options
end
- def read_group
- $stdout.puts
- $stdout.puts ">> Specify the group introducing the feature flag, like `group::project management`:"
+ def groups
+ @groups ||= fetch_json(WWW_GITLAB_COM_GROUPS_JSON)
+ end
- loop do
- group = Readline.readline('?> ', false)&.strip
- group = nil if group.empty?
- return group if group.nil? || group.start_with?('group::')
+ def rollout_issue_template
+ @rollout_issue_template ||= File.read(File.expand_path("../#{FF_ROLLOUT_ISSUE_TEMPLATE}", __dir__))
+ end
- $stderr.puts "The group needs to include `group::`"
+ def group_labels
+ @group_labels ||= groups.map { |_, group| group['label'] }.sort
+ end
+
+ def find_group_by_label(label)
+ groups.find { |_, group| group['label'] == label }[1]
+ end
+
+ def group_list
+ list = []
+ group_labels.each_with_index do |group_label, index|
+ list << "#{index + 1}. #{group_label}"
end
+
+ list.join("\n")
end
- def read_type
- # if there's only one type, do not ask, return
- return TYPES.first.first if TYPES.one?
+ def fetch_json(json_url)
+ json = with_retries { HTTParty.get(json_url, format: :plain) }
+ JSON.parse(json)
+ end
+
+ def with_retries(attempts: 3)
+ yield
+ rescue Errno::ECONNRESET, OpenSSL::SSL::SSLError, Net::OpenTimeout
+ retry if (attempts -= 1).positive?
+ raise
+ end
+ def read_type
$stdout.puts
$stdout.puts ">> Specify the feature flag type:"
$stdout.puts
- TYPES.each do |type, data|
+ TYPES.each_with_index do |(type, data), index|
next if data[:deprecated]
- $stdout.puts "#{type.to_s.rjust(15)}#{' '*6}#{data[:description]}"
+ $stdout.puts "#{index + 1}. #{type.to_s.rjust(17)} #{data[:description]}"
end
loop do
- type = Readline.readline('?> ', false)&.strip&.to_sym
- return type if TYPES[type] && !TYPES[type][:deprecated]
-
- $stderr.puts "Invalid type specified '#{type}'"
+ type = Readline.readline('?> ', false)&.strip
+ type = TYPES.keys[type.to_i - 1] unless type.to_i.zero?
+ type = type&.to_sym
+ type_def = TYPES[type]
+
+ if type_def && !type_def[:deprecated]
+ $stdout.puts "You picked the type '#{type}'"
+ return type
+ else
+ $stderr.puts "Invalid type specified '#{type}'"
+ end
end
end
- def read_introduced_by_url
+ def read_group
$stdout.puts
- $stdout.puts ">> URL of the MR introducing the feature flag (enter to skip):"
+ $stdout.puts ">> Specify the group label to which the feature flag belongs, from the following list:\n#{group_list}"
loop do
- introduced_by_url = Readline.readline('?> ', false)&.strip
- introduced_by_url = nil if introduced_by_url.empty?
- return introduced_by_url if introduced_by_url.nil? || introduced_by_url.start_with?('https://')
+ group = Readline.readline('?> ', false)&.strip
+ group = group_labels[group.to_i - 1] unless group.to_i.zero?
+
+ if group_labels.include?(group)
+ $stdout.puts "You picked the group '#{group}'"
+ return group
+ else
+ $stderr.puts "The group label isn't in the above labels list"
+ end
- $stderr.puts "URL needs to start with https://"
end
end
- def read_ee_only(options)
- TYPES.dig(options.type, :ee_only)
+ def read_feature_issue_url
+ read_url('URL of the original feature issue (enter to skip):')
+ end
+
+ def read_introduced_by_url
+ read_url('URL of the MR introducing the feature flag (enter to skip and let Danger provide a suggestion directly in the MR):')
end
def read_rollout_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}`"
+ issue_new_url = "https://gitlab.com/gitlab-com/gl-infra/production/-/issues/new"
+ issue_title = "[Feature flag] Rollout of `#{options.name}`"
+ issue_new_url = issue_new_url + "?" + URI.encode_www_form('issue[title]' => issue_title)
+ group_name = find_group_by_label(options.group)
- params = {
- 'issue[title]' => "[Feature flag] Rollout of `#{options.name}`",
- 'issuable_template' => 'Feature Flag Roll Out',
- }
- issue_new_url = url + "?" + URI.encode_www_form(params)
+ template = rollout_issue_template
+
+ if options.username
+ template.gsub!('<gitlab-username-of-dri>', options.username)
+ else
+ # Assign to current user by default
+ template.gsub!('/assign @<gitlab-username-of-dri>', "/assign me")
+ end
+
+ template.gsub!('<feature-flag-name>', options.name)
+ template.gsub!('<merge-request-url>', options.introduced_by_url) if options.introduced_by_url
+ template.gsub!('<milestone>', options.milestone)
+ template.gsub!('<feature-issue-link>', options.feature_issue_url) if options.feature_issue_url
+ template.gsub!('<slack-channel-of-dri-team>', group_name['slack_channel']) if group_name&.key?('slack_channel')
+ template.gsub!('<group-label>', %Q(~"#{options.group}"))
$stdout.puts
- $stdout.puts ">> Open this URL and fill in the rest of the details:"
- $stdout.puts issue_new_url
- $stdout.puts
+ $stdout.puts ">> Press any key and paste the issue content that we copied to your clipboard! 🚀"
+ Readline.readline('?> ', false)
+ copy_to_clipboard!(template)
+
+ if open_url!(issue_new_url) != 0
+ $stdout.puts ">> Automatic opening of the new issue URL failed, so please visit #{issue_new_url} manually."
+ end
$stdout.puts ">> URL of the rollout issue (enter to skip):"
loop do
created_url = Readline.readline('?> ', false)&.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://"
+ return created_url if created_url.nil? || valid_url?(created_url)
end
end
@@ -194,8 +274,65 @@ class FeatureFlagOptionParser
milestone.gsub(/^(\d+\.\d+).*$/, '\1').chomp
end
- def read_default_enabled(options)
- TYPES.dig(options.type, :default_enabled)
+ def read_username
+ $stdout.puts
+ $stdout.puts ">> Username of the feature flag DRI (enter to skip):"
+
+ loop do
+ username = Readline.readline('?> ', false)&.strip
+ return if username.empty?
+ return username if valid_url?("https://gitlab.com/#{username}")
+ end
+ end
+
+ def read_url(prompt)
+ $stdout.puts
+ $stdout.puts ">> #{prompt}"
+
+ loop do
+ url = Readline.readline('?> ', false)&.strip
+ url = nil if url.empty?
+ return url if url.nil? || valid_url?(url)
+ end
+ end
+
+ def valid_url?(url)
+ unless url.start_with?('https://')
+ $stderr.puts "URL needs to start with https://"
+ return false
+ end
+
+ response = HTTParty.head(url)
+
+ return true if response.success?
+
+ $stderr.puts "URL '#{url}' isn't valid!"
+ end
+
+ def open_url!(url)
+ _, open_url_status = Gitlab::Popen.popen([open_command, url])
+
+ open_url_status
+ end
+
+ def copy_to_clipboard!(text)
+ IO.popen(copy_to_clipboard_command.shellsplit, 'w') do |pipe|
+ pipe.print(text)
+ end
+ end
+
+ def copy_to_clipboard_command
+ find_compatible_command(COPY_COMMANDS)
+ end
+
+ def open_command
+ find_compatible_command(OPEN_COMMANDS)
+ end
+
+ def find_compatible_command(commands)
+ commands.find do |command|
+ Gitlab::Popen.popen(%W[which #{command.split(' ')[0]}])[1] == 0
+ end
end
end
end
@@ -216,11 +353,12 @@ class FeatureFlagCreator
# Read type from stdin unless is already set
options.type ||= FeatureFlagOptionParser.read_type
- options.ee ||= FeatureFlagOptionParser.read_ee_only(options)
options.group ||= FeatureFlagOptionParser.read_group
+ options.feature_issue_url ||= FeatureFlagOptionParser.read_feature_issue_url
options.introduced_by_url ||= FeatureFlagOptionParser.read_introduced_by_url
- options.rollout_issue_url ||= FeatureFlagOptionParser.read_rollout_issue_url(options)
options.milestone ||= FeatureFlagOptionParser.read_milestone
+ options.username ||= FeatureFlagOptionParser.read_username
+ options.rollout_issue_url ||= FeatureFlagOptionParser.read_rollout_issue_url(options)
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
$stdout.puts contents
@@ -238,22 +376,19 @@ class FeatureFlagCreator
private
def contents
- # Slice is used to ensure that YAML keys
- # are always ordered in a predictable way
- config_hash.slice(
- *::Feature::Shared::PARAMS.map(&:to_s)
- ).to_yaml
+ config_hash.to_yaml
end
def config_hash
{
'name' => options.name,
+ 'feature_issue_url' => options.feature_issue_url,
'introduced_by_url' => options.introduced_by_url,
'rollout_issue_url' => options.rollout_issue_url,
'milestone' => options.milestone,
'group' => options.group,
'type' => options.type.to_s,
- 'default_enabled' => FeatureFlagOptionParser.read_default_enabled(options)
+ 'default_enabled' => false
}
end
diff --git a/db/docs/batched_background_migrations/update_workspaces_config_version.yml b/db/docs/batched_background_migrations/update_workspaces_config_version.yml
index ead063ebed6..4ccddd5017a 100644
--- a/db/docs/batched_background_migrations/update_workspaces_config_version.yml
+++ b/db/docs/batched_background_migrations/update_workspaces_config_version.yml
@@ -3,3 +3,6 @@ description: Update config_version to 2 and force_include_all_resources to true
feature_category: remote_development
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131402
milestone: '16.5'
+queued_migration_version: 20230910120000
+finalize_after: "2023-11-15"
+finalized_by: 20231215151348
diff --git a/db/migrate/20231130124606_add_project_id_name_version_id_index_to_installable_terraform_modules.rb b/db/migrate/20231130124606_add_project_id_name_version_id_index_to_installable_terraform_modules.rb
new file mode 100644
index 00000000000..0b305843bd5
--- /dev/null
+++ b/db/migrate/20231130124606_add_project_id_name_version_id_index_to_installable_terraform_modules.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddProjectIdNameVersionIdIndexToInstallableTerraformModules < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'idx_pkgs_on_project_id_name_version_on_installable_terraform'
+ PACKAGE_TYPE_TERRAFORM_MODULE = 12
+ INSTALLABLE_CONDITION = 'status IN (0, 1)'
+
+ def up
+ add_concurrent_index(
+ :packages_packages,
+ %i[project_id name version id],
+ name: INDEX_NAME,
+ where: "package_type = #{PACKAGE_TYPE_TERRAFORM_MODULE} AND #{INSTALLABLE_CONDITION}"
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(:packages_packages, INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20231215151348_finalize_workspaces_config_version_2_migration.rb b/db/post_migrate/20231215151348_finalize_workspaces_config_version_2_migration.rb
new file mode 100644
index 00000000000..c76bc15d778
--- /dev/null
+++ b/db/post_migrate/20231215151348_finalize_workspaces_config_version_2_migration.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class FinalizeWorkspacesConfigVersion2Migration < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ MIGRATION = 'UpdateWorkspacesConfigVersion'
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: MIGRATION,
+ table_name: :workspaces,
+ column_name: :id,
+ job_arguments: [],
+ finalize: true
+ )
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20231130124606 b/db/schema_migrations/20231130124606
new file mode 100644
index 00000000000..ed5744a8c7e
--- /dev/null
+++ b/db/schema_migrations/20231130124606
@@ -0,0 +1 @@
+70f8264aa0996e3020fd068beba148f51170224126d1aa91740bd32bd59a196d \ No newline at end of file
diff --git a/db/schema_migrations/20231215151348 b/db/schema_migrations/20231215151348
new file mode 100644
index 00000000000..1c37bec18da
--- /dev/null
+++ b/db/schema_migrations/20231215151348
@@ -0,0 +1 @@
+281fde69710c20f9ae845136a4bfdbce1a8396f3d3d17018f7ffce1bf230b888 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3ed12685706..754c8139b37 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -31675,6 +31675,8 @@ CREATE INDEX idx_pkgs_installable_package_files_on_package_id_id_file_name ON pa
CREATE INDEX idx_pkgs_npm_metadata_caches_on_id_and_project_id_and_status ON packages_npm_metadata_caches USING btree (id) WHERE ((project_id IS NULL) AND (status = 0));
+CREATE INDEX idx_pkgs_on_project_id_name_version_on_installable_terraform ON packages_packages USING btree (project_id, name, version, id) WHERE ((package_type = 12) AND (status = ANY (ARRAY[0, 1])));
+
CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_cloud_last_sync_at, project_id) WHERE (jira_dvcs_cloud_last_sync_at IS NOT NULL);
CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_server_last_sync_at, project_id) WHERE (jira_dvcs_server_last_sync_at IS NOT NULL);
diff --git a/doc/administration/settings/jira_cloud_app_troubleshooting.md b/doc/administration/settings/jira_cloud_app_troubleshooting.md
index 1b6f88d43ae..7bbf232d412 100644
--- a/doc/administration/settings/jira_cloud_app_troubleshooting.md
+++ b/doc/administration/settings/jira_cloud_app_troubleshooting.md
@@ -26,12 +26,16 @@ To resolve this issue, set up [OAuth authentication](jira_cloud_app.md#set-up-oa
## Manual installation fails
-You might get an error if you have installed the GitLab for Jira Cloud app from the official marketplace listing and replaced it with [manual installation](jira_cloud_app.md#install-the-gitlab-for-jira-cloud-app-manually):
+You might see one of the following errors if you have installed the GitLab for Jira Cloud app from the official marketplace listing and replaced it with [manual installation](jira_cloud_app.md#install-the-gitlab-for-jira-cloud-app-manually):
```plaintext
The app "gitlab-jira-connect-gitlab.com" could not be installed as a local app as it has previously been installed from Atlassian Marketplace
```
+```plaintext
+The app host returned HTTP response code 401 when we tried to contact it during installation. Please try again later or contact the app vendor.
+```
+
To resolve this issue, disable the **Jira Connect Proxy URL** setting.
- In GitLab 15.7:
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 46ddad63360..3701abbc237 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -19166,7 +19166,7 @@ GPG signature for a signed commit.
| <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
-| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
+| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
| <a id="groupcrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. |
| <a id="groupdependencyproxyblobcount"></a>`dependencyProxyBlobCount` | [`Int!`](#int) | Number of dependency proxy blobs cached in the group. |
| <a id="groupdependencyproxyblobs"></a>`dependencyProxyBlobs` | [`DependencyProxyBlobConnection`](#dependencyproxyblobconnection) | Dependency Proxy blobs. (see [Connections](#connections)) |
@@ -19191,7 +19191,7 @@ GPG signature for a signed commit.
| <a id="groupfullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace. |
| <a id="groupgooglecloudloggingconfigurations"></a>`googleCloudLoggingConfigurations` | [`GoogleCloudLoggingConfigurationTypeConnection`](#googlecloudloggingconfigurationtypeconnection) | Google Cloud logging configurations that receive audit events belonging to the group. (see [Connections](#connections)) |
| <a id="groupid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
-| <a id="groupistemporarystorageincreaseenabled"></a>`isTemporaryStorageIncreaseEnabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.7. Feature removal, will be completely removed in 17.0. |
+| <a id="groupistemporarystorageincreaseenabled"></a>`isTemporaryStorageIncreaseEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 16.7. Feature removal, will be completely removed in 17.0. |
| <a id="grouplfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
| <a id="groupmentionsdisabled"></a>`mentionsDisabled` | [`Boolean`](#boolean) | Indicates if a group is disabled from getting mentioned. |
| <a id="groupname"></a>`name` | [`String!`](#string) | Name of the namespace. |
@@ -19201,7 +19201,7 @@ GPG signature for a signed commit.
| <a id="grouppendingmembers"></a>`pendingMembers` **{warning-solid}** | [`PendingGroupMemberConnection`](#pendinggroupmemberconnection) | **Introduced** in 16.6. This feature is an Experiment. It can be changed or removed at any time. A pending membership of a user within this group. |
| <a id="groupprojectcreationlevel"></a>`projectCreationLevel` | [`String`](#string) | Permission level required to create projects in the group. |
| <a id="grouprecentissueboards"></a>`recentIssueBoards` | [`BoardConnection`](#boardconnection) | List of recently visited boards of the group. Maximum size is 4. (see [Connections](#connections)) |
-| <a id="grouprepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int!`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
+| <a id="grouprepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
| <a id="grouprequestaccessenabled"></a>`requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request access to namespace. |
| <a id="grouprequiretwofactorauthentication"></a>`requireTwoFactorAuthentication` | [`Boolean`](#boolean) | Indicates if all users in this group are required to set up two-factor authentication. |
| <a id="grouprootstoragestatistics"></a>`rootStorageStatistics` | [`RootStorageStatistics`](#rootstoragestatistics) | Aggregated storage statistics of the namespace. Only available for root namespaces. |
@@ -22962,19 +22962,19 @@ Product analytics events for a specific month and year.
| <a id="namespaceactualrepositorysizelimit"></a>`actualRepositorySizeLimit` | [`Float`](#float) | Size limit for repositories in the namespace in bytes. This limit only applies to namespaces under Project limit enforcement. |
| <a id="namespaceactualsizelimit"></a>`actualSizeLimit` | [`Float`](#float) | The actual storage size limit (in bytes) based on the enforcement type of either repository or namespace. This limit is agnostic of enforcement type. |
| <a id="namespaceadditionalpurchasedstoragesize"></a>`additionalPurchasedStorageSize` | [`Float`](#float) | Additional storage purchased for the root namespace in bytes. |
-| <a id="namespacecontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
+| <a id="namespacecontainslockedprojects"></a>`containsLockedProjects` | [`Boolean`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
| <a id="namespacecrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. |
| <a id="namespacedescription"></a>`description` | [`String`](#string) | Description of the namespace. |
| <a id="namespacedescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="namespacefullname"></a>`fullName` | [`String!`](#string) | Full name of the namespace. |
| <a id="namespacefullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace. |
| <a id="namespaceid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
-| <a id="namespaceistemporarystorageincreaseenabled"></a>`isTemporaryStorageIncreaseEnabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.7. Feature removal, will be completely removed in 17.0. |
+| <a id="namespaceistemporarystorageincreaseenabled"></a>`isTemporaryStorageIncreaseEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 16.7. Feature removal, will be completely removed in 17.0. |
| <a id="namespacelfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
| <a id="namespacename"></a>`name` | [`String!`](#string) | Name of the namespace. |
| <a id="namespacepackagesettings"></a>`packageSettings` | [`PackageSettings`](#packagesettings) | Package settings for the namespace. |
| <a id="namespacepath"></a>`path` | [`String!`](#string) | Path of the namespace. |
-| <a id="namespacerepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int!`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
+| <a id="namespacerepositorysizeexcessprojectcount"></a>`repositorySizeExcessProjectCount` | [`Int`](#int) | Number of projects in the root namespace where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
| <a id="namespacerequestaccessenabled"></a>`requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request access to namespace. |
| <a id="namespacerootstoragestatistics"></a>`rootStorageStatistics` | [`RootStorageStatistics`](#rootstoragestatistics) | Aggregated storage statistics of the namespace. Only available for root namespaces. |
| <a id="namespacesharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
diff --git a/doc/api/packages/terraform-modules.md b/doc/api/packages/terraform-modules.md
index 3645fc03e9a..079b7652e67 100644
--- a/doc/api/packages/terraform-modules.md
+++ b/doc/api/packages/terraform-modules.md
@@ -208,6 +208,8 @@ X-Terraform-Get: /api/v4/packages/terraform/modules/v1/group/hello-world/local/1
## Download module
+### From a namespace
+
```plaintext
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file
```
@@ -229,6 +231,29 @@ To write the output to file:
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file" --output hello-world-local.tgz
```
+### From a project
+
+```plaintext
+GET /projects/:id/packages/terraform/modules/:module_name/:module_system/:module_version
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or URL-encoded path of the project. |
+| `module_name` | string | yes | The module name. |
+| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
+| `module_version` | string | no | Specific module version to download. If omitted, the latest version is downloaded. |
+
+```shell
+curl --user "<username>:<personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/terraform/modules/hello-world/local/1.0.0"
+```
+
+To write the output to file:
+
+```shell
+curl --user "<username>:<personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/terraform/modules/hello-world/local/1.0.0" --output hello-world-local.tgz
+```
+
## Upload module
```plaintext
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index 17dd8432f7b..965b3bd86aa 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -7,7 +7,7 @@ info: "See the Technical Writers assigned to Development Guidelines: https://han
# Feature flags in the development of GitLab
NOTE:
-This document explains how to contribute to the development of the GitLab product.
+This document explains how to contribute to the development and operations of the GitLab product.
If you want to use feature flags to show and hide functionality in your own applications,
view [this feature flags information](../../operations/feature_flags.md) instead.
@@ -17,6 +17,11 @@ All newly-introduced feature flags should be [disabled by default](https://about
WARNING:
All newly-introduced feature flags should be [used with an actor](controls.md#percentage-based-actor-selection).
+Blueprints:
+
+- (Latest) [Feature Flags usage in GitLab development and operations](../../architecture/blueprints/feature_flags_usage_in_dev_and_ops/index.md)
+- [Development Feature Flags Architecture](../../architecture/blueprints/feature_flags_development/index.md)
+
This document is the subject of continued work as part of an epic to [improve internal usage of feature flags](https://gitlab.com/groups/gitlab-org/-/epics/3551). Raise any suggestions as new issues and attach them to the epic.
For an [overview of the feature flag lifecycle](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#feature-flag-lifecycle), or if you need help deciding [if you should use a feature flag](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags) or not, see the [feature flag lifecycle](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/) handbook page.
@@ -44,77 +49,147 @@ should be leveraged:
When the feature implementation is delivered over multiple merge requests:
1. [Create a new feature flag](#create-a-new-feature-flag)
- which is **off** by default, in the first merge request which uses the flag.
- Flags [should not be added separately](#risk-of-a-broken-main-branch).
+ which is **disabled** by default, in the first merge request which uses the flag.
+ Flags [should not be added separately](#risk-of-a-broken-default-branch).
1. Submit incremental changes via one or more merge requests, ensuring that any
- new code added can only be reached if the feature flag is **on**.
+ new code added can only be reached if the feature flag is **enabled**.
You can keep the feature flag enabled on your local GDK during development.
1. When the feature is ready to be tested by other team members, [create the initial documentation](../documentation/feature_flags.md#when-to-document-features-behind-a-feature-flag).
Include details about the status of the [feature flag](../documentation/feature_flags.md#how-to-add-feature-flag-documentation).
-1. Enable the feature flag for a specific project and ensure that there are no issues
+1. Enable the feature flag for a specific group/project/user and ensure that there are no issues
with the implementation. Do not enable the feature flag for a public project
- like `gitlab` if there is no documentation. Team members and contributors might search for
+ like `gitlab-org/gitlab` if there is no documentation. Team members and contributors might search for
documentation on how to use the feature if they see it enabled in a public project.
1. When the feature is ready for production use, open a merge request to:
- Update the documentation to describe the latest flag status.
- Add a [changelog entry](#changelog).
- - Flip the feature flag to be **on by default** or remove it entirely
- to enable the new behavior.
+ - Remove the feature flag to enable the new behavior, or flip the feature flag to be **enabled by default** (only for `ops` and `beta` feature flags).
One might be tempted to think that feature flags will delay the release of a
feature by at least one month (= one release). This is not the case. A feature
flag does not have to stick around for a specific amount of time
(for example, at least one release), instead they should stick around until the feature
-is deemed stable. Stable means it works on GitLab.com without causing any
-problems, such as outages.
+is deemed stable. **Stable means it works on GitLab.com without causing any
+problems, such as outages.**
-## Risk of a broken main branch
+## Risk of a broken default branch
Feature flags must be used in the MR that introduces them. Not doing so causes a
-[broken main branch](https://about.gitlab.com/handbook/engineering/workflow/#broken-master) scenario due
-to the `rspec:feature-flags` job that only runs on the `main` branch.
+[broken default branch](https://about.gitlab.com/handbook/engineering/workflow/#broken-master) scenario due
+to the `rspec:feature-flags` job that only runs on the default branch.
## Types of feature flags
Choose a feature flag type that matches the expected usage.
-### `development` type
+### `gitlab_com_derisk` type
+
+`gitlab_com_derisk` feature flags are short-lived feature flags,
+used to de-risk GitLab.com deployments. Most feature flags used at
+GitLab are of the `gitlab_com_derisk` type.
+
+#### Constraints
+
+- `default_enabled`: **Must not** be set to true. This kind of feature flag is meant to lower the risk on GitLab.com, thus there's no need to keep the flag in the codebase after it's been enabled on GitLab.com. `default_enabled: true` will not have any effect for this type of feature flag.
+- Maximum Lifespan: 2 months after it's merged into the default branch
+- Documentation: This type of feature flag don't need to be documented in the
+ [All feature flags in GitLab](../../user/feature_flags.md) page given they're short-lived and deployment-related
+- Rollout issue: **Must** have a rollout issue created from the
+ [Feature flag Roll Out template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md)
-`development` feature flags are short-lived feature flags,
-used for deploying unfinished code to production. Most feature flags used at
-GitLab are the `development` type.
+#### Usage
-A `development` feature flag must have a rollout issue
-created from the [Feature flag Roll Out template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md).
+The format for `gitlab_com_derisk` feature flags is `Feature.<state>(:<dev_flag_name>)`.
-The format for `development` feature flags is `Feature.<state>(:<dev_flag_name>)`.
To enable and disable them, run on the GitLab Rails console:
```ruby
# To enable it for the instance:
-Feature.enable(:<dev_flag_name>)
+Feature.enable(:<dev_flag_name>, type: :gitlab_com_derisk)
# To disable it for the instance:
-Feature.disable(:<dev_flag_name>)
+Feature.disable(:<dev_flag_name>, type: :gitlab_com_derisk)
# To enable for a specific project:
-Feature.enable(:<dev_flag_name>, Project.find(<project id>))
+Feature.enable(:<dev_flag_name>, Project.find(<project id>), type: :gitlab_com_derisk)
# To disable for a specific project:
-Feature.disable(:<dev_flag_name>, Project.find(<project id>))
+Feature.disable(:<dev_flag_name>, Project.find(<project id>), type: :gitlab_com_derisk)
```
-To check a `development` feature flag's state:
+To check a `gitlab_com_derisk` feature flag's state:
```ruby
# Check if the feature flag is enabled
-Feature.enabled?(:dev_flag_name)
+Feature.enabled?(:dev_flag_name, type: :gitlab_com_derisk)
# Check if the feature flag is disabled
-Feature.disabled?(:dev_flag_name)
+Feature.disabled?(:dev_flag_name, type: :gitlab_com_derisk)
+```
+
+### `wip` type
+
+Some features are complex and need to be implemented through several MRs. Until they're fully implemented,
+it needs to be hidden from anyone. In that case, the `wip` (for "Work In Progress") feature flag allows
+to merge all the changes to the main branch without actually using the feature yet.
+
+Once the feature is complete, the feature flag type can be changed to the `gitlab_com_derisk` or
+`beta` type depending on how the feature will be presented/documented to customers.
+
+#### Constraints
+
+- `default_enabled`: **Must not** be set to true. If needed, this type can be changed to beta once the feature is complete.
+- Maximum Lifespan: 4 months after it's merged into the default branch
+- Documentation: This type of feature flag don't need to be documented in the
+ [All feature flags in GitLab](../../user/feature_flags.md) page given they're mostly hiding unfinished code
+- Rollout issue: Likely no need for a rollout issues, as `wip` feature flags should be transitioned to
+ another type before being enabled
+
+#### Usage
+
+```ruby
+# Check if feature flag is enabled
+Feature.enabled?(:my_wip_flag, project, type: :wip)
+
+# Check if feature flag is disabled
+Feature.disabled?(:my_wip_flag, project, type: :wip)
+
+# Push feature flag to Frontend
+push_frontend_feature_flag(:my_wip_flag, project, type: :wip)
```
-For `development` feature flags, the type doesn't need to be specified (they're the default type).
+### `beta` type
+
+We might
+[not be confident we'll be able to scale, support, and maintain a feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#experiment-beta-ga)
+in its current form for every designed use case ([example](https://gitlab.com/gitlab-org/gitlab/-/issues/336070#note_1523983444)).
+There are also scenarios where a feature is not complete enough to be considered an MVC.
+Providing a flag in this case allows engineers and customers to disable the new feature until it's performant enough.
+
+#### Constraints
+
+- `default_enabled`: Can be set to `true` so that a feature can be "released" to everyone in Beta with the
+ possibility to disable it in the case of scalability issues (ideally it should only be disabled for this
+ reason on specific on-premise installations)
+- Maximum Lifespan: 6 months after it's merged into the default branch
+- Documentation: This type of feature flag **must** be documented in the
+ [All feature flags in GitLab](../../user/feature_flags.md) page
+- Rollout issue: **Must** have a rollout issue
+ created from the
+ [Feature flag Roll Out template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md)
+
+#### Usage
+
+```ruby
+# Check if feature flag is enabled
+Feature.enabled?(:my_beta_flag, project, type: :beta)
+
+# Check if feature flag is disabled
+Feature.disabled?(:my_beta_flag, project, type: :beta)
+
+# Push feature flag to Frontend
+push_frontend_feature_flag(:my_beta_flag, project, type: :beta)
+```
### `ops` type
@@ -122,10 +197,20 @@ For `development` feature flags, the type doesn't need to be specified (they're
of GitLab product behavior. For example, feature flags that disable features that might
have a performance impact such as Sidekiq worker behavior.
-`ops` feature flags likely do not have rollout issues, as it is hard to
-predict when they are enabled or disabled.
+Remember that using this type should follow a conscious decision not to introduce an
+instance/group/project/user setting.
+
+#### Constraints
+
+- `default_enabled`: Can be set to `true` so that a feature can be "released" to everyone in Beta with the
+ possibility to disable it in the case of scalability issues (ideally it should only be disabled for this
+ reason on specific on-premise installations)
+- Maximum Lifespan: Unlimited
+- Documentation: This type of feature flag **must** be documented in the
+ [All feature flags in GitLab](../../user/feature_flags.md) page
+- Rollout issue: Likely no need for a rollout issues, as it is hard to predict when they are enabled or disabled
-To invoke `ops` feature flags, you must append `type: :ops`:
+#### Usage
```ruby
# Check if feature flag is enabled
@@ -142,18 +227,27 @@ push_frontend_feature_flag(:my_ops_flag, project, type: :ops)
`experiment` feature flags are used for A/B testing on GitLab.com.
-An `experiment` feature flag should conform to the same standards as a `development` feature flag,
+An `experiment` feature flag should conform to the same standards as a `beta` feature flag,
although the interface has some differences. An experiment feature flag should have a rollout issue,
created using the [Experiment Tracking template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Experiment%20Rollout.md). More information can be found in the [experiment guide](../experiment_guide/index.md).
+#### Constraints
+
+- `default_enabled`: **Must not** be set to `true`.
+- Maximum Lifespan: 6 months after it's merged into the default branch
+
### `worker` type
-`worker` feature flags are used for controlling Sidekiq workers behavior, such as deferring Sidekiq jobs.
+`worker` feature flags are special `ops` flags that allow to control Sidekiq workers behavior, such as deferring Sidekiq jobs.
`worker` feature flags likely do not have any YAML definition as the name could be dynamically generated using
the worker name itself, for example, `run_sidekiq_jobs_AuthorizedProjectsWorker`. Some examples for using `worker` type feature
flags can be found in [deferring Sidekiq jobs](#deferring-sidekiq-jobs).
+### [Deprecated]`development` type
+
+The `development` type is deprecated in favor of the `gitlab_com_derisk`, `wip`, and `beta` feature flag types.
+
## Feature flag definition and validation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229161) in GitLab 13.3.
@@ -179,10 +273,11 @@ Each feature flag is defined in a separate YAML file consisting of a number of f
| `name` | yes | Name of the feature flag. |
| `type` | yes | Type of feature flag. |
| `default_enabled` | yes | The default state of the feature flag. |
-| `introduced_by_url` | no | The URL to the merge request that introduced the feature flag. |
+| `introduced_by_url` | yes | The URL to the merge request that introduced the feature flag. |
+| `milestone` | yes | Milestone in which the feature flag was created. |
+| `group` | yes | The [group](https://about.gitlab.com/handbook/product/categories/#devops-stages) that owns the feature flag. |
+| `feature_issue_url` | no | The URL to the original feature issue. |
| `rollout_issue_url` | no | The URL to the Issue covering the feature flag rollout. |
-| `milestone` | no | Milestone in which the feature flag was created. |
-| `group` | no | The [group](https://about.gitlab.com/handbook/product/categories/#devops-stages) that owns the feature flag. |
NOTE:
All validations are skipped when running in `RAILS_ENV=production`.
diff --git a/doc/development/work_items.md b/doc/development/work_items.md
index 0c3bc4611f5..7fc0c0d9d21 100644
--- a/doc/development/work_items.md
+++ b/doc/development/work_items.md
@@ -245,7 +245,8 @@ Keep the following in mind when you write your migration:
necessary if the new work item type is going to use the `Hierarchy` widget. In this table, you must add what
work item type can have children and of what type. Also, you should specify the hierarchy depth for work items of the same
type. By default a cross-hierarchy (cross group or project) relationship is disabled when creating new restrictions but
- it can be enabled by specifying a value for `cross_hierarchy_enabled`.
+ it can be enabled by specifying a value for `cross_hierarchy_enabled`. Due to the restrictions being cached for the work item type, it's also
+ required to call `clear_reactive_cache!` on the associated work item types.
- Optional. Create linked item restrictions.
- Similarly to the `Hierarchy` widget, the `Linked items` widget also supports rules defining which work item types can be
linked to other types. A restriction can specify if the source type can be related to or blocking a target type. Current restrictions:
diff --git a/doc/user/packages/terraform_module_registry/index.md b/doc/user/packages/terraform_module_registry/index.md
index 2c9576bf9f7..8612acc37cb 100644
--- a/doc/user/packages/terraform_module_registry/index.md
+++ b/doc/user/packages/terraform_module_registry/index.md
@@ -163,7 +163,9 @@ Prerequisites:
- You need to [authenticate with the API](../../../api/rest/index.md#authentication). If authenticating with a personal access token, it must be configured with the `read_api` scope.
-Authentication tokens (Job Token or Personal Access Token) can be provided for `terraform` in your `~/.terraformrc` or `%APPDATA%/terraform.rc` file:
+### From a namespace
+
+You can provide authentication tokens (job tokens, personal access tokens, or deploy tokens) for `terraform` in your `~/.terraformrc` or `%APPDATA%/terraform.rc` file:
```terraform
credentials "gitlab.com" {
@@ -183,6 +185,30 @@ module "<module>" {
Where `<namespace>` is the [namespace](../../../user/namespace/index.md) of the Terraform Module Registry.
+### From a project
+
+To reference a Terraform module using a project-level source, use the [fetching archives over HTTP](https://developer.hashicorp.com/terraform/language/modules/sources#fetching-archives-over-http) source type provided by Terraform.
+
+You can provide authentication tokens (job tokens, personal access tokens, or deploy tokens) for `terraform` in your `~/.netrc` file:
+
+```netrc
+machine gitlab.com
+login <USERNAME>
+password <TOKEN>
+```
+
+Where `gitlab.com` can be replaced with the hostname of your self-managed GitLab instance, and `<USERNAME>` is your token username.
+
+You can refer to your Terraform module from a downstream Terraform project:
+
+```terraform
+module "<module>" {
+ source = "https://gitlab.com/api/v4/projects/<project-id>/packages/terraform/modules/<module-name>/<module-system>/<module-version>"
+}
+```
+
+If you need to reference the latest version of a module, you can omit the `<module-version>` from the source URL. To prevent future issues, you should reference a specific version if possible.
+
## Download a Terraform module
To download a Terraform module:
diff --git a/doc/user/project/pages/pages_access_control.md b/doc/user/project/pages/pages_access_control.md
index 1c7aa0f182c..07b41f391ba 100644
--- a/doc/user/project/pages/pages_access_control.md
+++ b/doc/user/project/pages/pages_access_control.md
@@ -48,7 +48,8 @@ can access the website.
To sign out of your GitLab Pages website, revoke the application access token
for GitLab Pages:
-1. In the top menu, select your profile, and then select **Settings**.
-1. On the left sidebar, select **Applications**.
-1. Scroll to the **Authorized applications** section, find the **GitLab Pages**
- entry, and select its **Revoke** button.
+1. On the left sidebar, select your avatar.
+1. Select **Edit profile**.
+1. Select **Applications**.
+1. In the **Authorized applications** section, find the **GitLab Pages**
+ entry, and select **Revoke**.
diff --git a/lib/api/terraform/modules/v1/project_packages.rb b/lib/api/terraform/modules/v1/project_packages.rb
index 07dfddefefc..ff330b8479f 100644
--- a/lib/api/terraform/modules/v1/project_packages.rb
+++ b/lib/api/terraform/modules/v1/project_packages.rb
@@ -16,87 +16,174 @@ module API
require_packages_enabled!
end
- params do
- requires :id, type: String, desc: 'The ID or full path of a project'
- requires :module_name, type: String, desc: "", regexp: API::NO_SLASH_URL_PART_REGEX
- requires :module_system, type: String, regexp: API::NO_SLASH_URL_PART_REGEX
- requires :module_version, type: String, desc: 'Module version', regexp: Gitlab::Regex.semver_regex
- end
+ helpers do
+ params :terraform_get do
+ optional 'terraform-get', type: String, values: %w[1], desc: 'Terraform get redirection flag'
+ end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- namespace ':id/packages/terraform/modules/:module_name/:module_system/*module_version/file' do
- authenticate_with do |accept|
- accept.token_types(:deploy_token).sent_through(:http_deploy_token_header)
- accept.token_types(:job_token).sent_through(:http_job_token_header)
- accept.token_types(:personal_access_token).sent_through(:http_private_token_header)
+ def present_package_file
+ authorize_read_package!(authorized_user_project)
+
+ if declared_params[:'terraform-get'] == '1'
+ header 'X-Terraform-Get', "#{request.url.split('?').first}?archive=tgz"
+ return no_content!
end
- desc 'Workhorse authorize Terraform Module package file' do
- detail 'This feature was introduced in GitLab 13.11'
- success code: 200
- failure [
- { code: 403, message: 'Forbidden' }
- ]
- tags %w[terraform_registry]
+ package = ::Packages::TerraformModule::PackagesFinder
+ .new(authorized_user_project, finder_params)
+ .execute
+ .first
+
+ not_found! unless package
+
+ track_package_event('pull_package', :terraform_module, project: authorized_user_project,
+ namespace: authorized_user_project.namespace)
+
+ present_package_file!(package.installable_package_files.first)
+ end
+
+ def finder_params
+ { package_name: package_name }.tap do |finder_params|
+ finder_params[:package_version] = params[:module_version] if params.key?(:module_version)
end
+ end
+
+ def package_name
+ "#{params[:module_name]}/#{params[:module_system]}"
+ end
+ end
- put 'authorize' do
- authorize_workhorse!(
- subject: authorized_user_project,
- maximum_size: authorized_user_project.actual_limits.terraform_module_max_file_size
- )
+ params do
+ requires :id, types: [String, Integer], allow_blank: false, desc: 'The ID or full path of a project'
+ with(type: String, allow_blank: false, regexp: API::NO_SLASH_URL_PART_REGEX) do
+ requires :module_name, desc: 'Module name', documentation: { example: 'infra-registry' }
+ requires :module_system, desc: 'Module system', documentation: { example: 'aws' }
+ end
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace ':id/packages/terraform/modules/:module_name/:module_system' do
+ authenticate_with do |accept|
+ accept.token_types(
+ :personal_access_token_with_username,
+ :deploy_token_with_username,
+ :job_token_with_username
+ ).sent_through(:http_basic_auth)
end
- desc 'Upload Terraform Module package file' do
- detail 'This feature was introduced in GitLab 13.11'
- success code: 201
+ desc 'Download the latest version of a module' do
+ detail 'This feature was introduced in GitLab 16.7'
+ success code: 204
failure [
- { code: 400, message: 'Invalid file' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }
]
- consumes %w[multipart/form-data]
tags %w[terraform_registry]
end
-
params do
- requires :file, type: ::API::Validations::Types::WorkhorseFile,
- desc: 'The package file to be published (generated by Multipart middleware)',
- documentation: { type: 'file' }
+ use :terraform_get
+ end
+ get do
+ present_package_file
end
- put do
- authorize_upload!(authorized_user_project)
-
- bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(
- :terraform_module_max_file_size, params[:file].size)
-
- create_package_file_params = {
- module_name: params['module_name'],
- module_system: params['module_system'],
- module_version: params['module_version'],
- file: params['file'],
- build: current_authenticated_job
- }
-
- result = ::Packages::TerraformModule::CreatePackageService
- .new(authorized_user_project, current_user, create_package_file_params)
- .execute
-
- render_api_error!(result[:message], result[:http_status]) if result[:status] == :error
-
- track_package_event('push_package', :terraform_module, project: authorized_user_project,
- namespace: authorized_user_project.namespace)
-
- created!
- rescue ObjectStorage::RemoteStoreError => e
- Gitlab::ErrorTracking.track_exception(
- e,
- extra: { file_name: params[:file_name], project_id: authorized_user_project.id }
- )
-
- forbidden!
+ params do
+ requires :module_version, type: String, allow_blank: false, desc: 'Module version',
+ regexp: Gitlab::Regex.semver_regex
+ end
+ namespace '*module_version' do
+ desc 'Download a specific version of a module' do
+ detail 'This feature was introduced in GitLab 16.7'
+ success code: 204
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags %w[terraform_registry]
+ end
+ params do
+ use :terraform_get
+ end
+ get format: false do
+ present_package_file
+ end
+
+ namespace :file do
+ authenticate_with do |accept|
+ accept.token_types(:deploy_token).sent_through(:http_deploy_token_header)
+ accept.token_types(:job_token).sent_through(:http_job_token_header)
+ accept.token_types(:personal_access_token).sent_through(:http_private_token_header)
+ end
+
+ desc 'Workhorse authorize Terraform Module package file' do
+ detail 'This feature was introduced in GitLab 13.11'
+ success code: 200
+ failure [
+ { code: 403, message: 'Forbidden' }
+ ]
+ tags %w[terraform_registry]
+ end
+
+ put :authorize do
+ authorize_workhorse!(
+ subject: authorized_user_project,
+ maximum_size: authorized_user_project.actual_limits.terraform_module_max_file_size
+ )
+ end
+
+ desc 'Upload Terraform Module package file' do
+ detail 'This feature was introduced in GitLab 13.11'
+ success code: 201
+ failure [
+ { code: 400, message: 'Invalid file' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' }
+ ]
+ consumes %w[multipart/form-data]
+ tags %w[terraform_registry]
+ end
+
+ params do
+ requires :file, type: ::API::Validations::Types::WorkhorseFile,
+ desc: 'The package file to be published (generated by Multipart middleware)',
+ documentation: { type: 'file' }
+ end
+
+ put do
+ authorize_upload!(authorized_user_project)
+
+ bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(
+ :terraform_module_max_file_size, params[:file].size
+ )
+
+ create_package_file_params = {
+ module_name: params['module_name'],
+ module_system: params['module_system'],
+ module_version: params['module_version'],
+ file: params['file'],
+ build: current_authenticated_job
+ }
+
+ result = ::Packages::TerraformModule::CreatePackageService
+ .new(authorized_user_project, current_user, create_package_file_params)
+ .execute
+
+ render_api_error!(result[:message], result[:http_status]) if result[:status] == :error
+
+ track_package_event('push_package', :terraform_module, project: authorized_user_project,
+ namespace: authorized_user_project.namespace)
+
+ created!
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e,
+ extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
+
+ forbidden!
+ end
+ end
end
end
end
diff --git a/lib/feature/definition.rb b/lib/feature/definition.rb
index af60fb95c53..14848f22f83 100644
--- a/lib/feature/definition.rb
+++ b/lib/feature/definition.rb
@@ -49,23 +49,27 @@ module Feature
end
unless type.present?
- raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing type. Ensure to update #{path}"
+ 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
+ if File.basename(path, ".yml") != name || File.basename(File.dirname(path)) != type
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
+ validate_default_enabled!
+ end
+ def validate_default_enabled!
if default_enabled.nil?
- raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing default_enabled. Ensure to update #{path}"
+ raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing `default_enabled`. Ensure to update #{path}"
+ end
+
+ if default_enabled && !Definition::TYPES.dig(type.to_sym, :can_be_default_enabled)
+ raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' cannot have `default_enabled` set to `true`. Ensure to update #{path}"
end
end
diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb
index d801070ff1a..fcce2642ef6 100644
--- a/lib/feature/shared.rb
+++ b/lib/feature/shared.rb
@@ -8,21 +8,39 @@ module Feature
module Shared
# optional: defines if a on-disk definition is required for this feature flag type
# rollout_issue: defines if `bin/feature-flag` asks for rollout issue
- # default_enabled: defines a default state of a feature flag when created by `bin/feature-flag`
- # ee_only: defines that a feature flag can only be created in a context of EE
+ # can_be_default_enabled: whether the flag can have `default_enabled` set to `true` or not
# deprecated: defines if a feature flag type that is deprecated and to be removed,
# the deprecated types are hidden from all interfaces
# example: usage being shown when exception is raised
TYPES = {
- development: {
- description: 'Short lived, used to enable unfinished code to be deployed',
+ gitlab_com_derisk: {
+ description: 'Short lived, used to de-risk GitLab.com deployments',
+ optional: false,
+ rollout_issue: true,
+ can_be_default_enabled: false,
+ example: <<-EOS
+ Feature.enabled?(:my_feature_flag, project, type: :gitlab_com_derisk)
+ push_frontend_feature_flag(:my_feature_flag, project)
+ EOS
+ },
+ wip: {
+ description: 'Used to hide unfinished code from anyone',
+ optional: false,
+ rollout_issue: false,
+ can_be_default_enabled: false,
+ example: <<-EOS
+ Feature.enabled?(:my_feature_flag, project, type: :wip)
+ push_frontend_feature_flag(:my_feature_flag, project)
+ EOS
+ },
+ beta: {
+ description: "Use when we aren't confident about scaling/supporting a feature, " \
+ "or when it isn't complete enough for an MVC",
optional: false,
rollout_issue: true,
- ee_only: false,
- default_enabled: false,
+ can_be_default_enabled: true,
example: <<-EOS
- Feature.enabled?(:my_feature_flag, project)
- Feature.enabled?(:my_feature_flag, project, type: :development)
+ Feature.enabled?(:my_feature_flag, project, type: :beta)
push_frontend_feature_flag(:my_feature_flag, project)
EOS
},
@@ -30,27 +48,17 @@ module Feature
description: "Long-lived feature flags that control operational aspects of GitLab's behavior",
optional: false,
rollout_issue: true,
- ee_only: false,
- default_enabled: false,
+ can_be_default_enabled: true,
example: <<-EOS
Feature.enabled?(:my_ops_flag, type: :ops)
push_frontend_feature_flag(:my_ops_flag, project, type: :ops)
EOS
},
- undefined: {
- description: "Feature flags that are undefined in GitLab codebase (should not be used)",
- optional: true,
- rollout_issue: false,
- ee_only: false,
- default_enabled: false,
- example: ''
- },
experiment: {
description: 'Short lived, used specifically to run A/B/n experiments.',
optional: true,
rollout_issue: true,
- ee_only: false,
- default_enabled: false,
+ can_be_default_enabled: false,
example: <<-EOS
experiment(:my_experiment, project: project, actor: current_user) { ...variant code... }
EOS
@@ -59,12 +67,22 @@ module Feature
description: "Feature flags for controlling Sidekiq workers behavior (e.g. deferring jobs)",
optional: true,
rollout_issue: false,
- ee_only: false,
- default_enabled: false,
+ can_be_default_enabled: false,
example: '<<-EOS
Feature.enabled?(:"defer_sidekiq_jobs:AuthorizedProjectsWorker", type: :worker,
default_enabled_if_undefined: false)
EOS'
+ },
+ undefined: {
+ description: "Feature flags that are undefined in GitLab codebase (should not be used)",
+ optional: true,
+ rollout_issue: false,
+ can_be_default_enabled: false,
+ example: ''
+ },
+ development: {
+ deprecated: true,
+ can_be_default_enabled: true
}
}.freeze
@@ -72,6 +90,7 @@ module Feature
# This is done to ease the file comparison
PARAMS = %i[
name
+ feature_issue_url
introduced_by_url
rollout_issue_url
milestone
diff --git a/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb b/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb
index 4e3b685c06c..ec0d9c3f36e 100644
--- a/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb
+++ b/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer.rb
@@ -66,7 +66,10 @@ module Gitlab
def self.find_or_create_type(name)
type = ::WorkItems::Type.find_by_name_and_namespace_id(name, nil)
- return type if type
+ if type
+ type.clear_reactive_cache!
+ return type
+ end
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
::WorkItems::Type.find_by_name_and_namespace_id(name, nil)
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 2f7fa89019e..e4b195767ea 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -197,12 +197,12 @@ module Gitlab
@updates[:subscription_event] = 'unsubscribe'
end
- desc { _('Toggle emoji award') }
+ desc { _('Toggle emoji reaction') }
explanation do |name|
- _("Toggles :%{name}: emoji award.") % { name: name } if name
+ _("Toggles :%{name}: emoji reaction.") % { name: name } if name
end
execution_message do |name|
- _("Toggled :%{name}: emoji award.") % { name: name } if name
+ _("Toggled :%{name}: emoji reaction.") % { name: name } if name
end
params ':emoji:'
types ::Issuable
@@ -213,7 +213,7 @@ module Gitlab
match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern)
match[1] if match
end
- command :award, :react do |name|
+ command :react, :award do |name|
if name && quick_action_target.user_can_award?(current_user)
@updates[:emoji_award] = name
end
diff --git a/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter.rb
index 557179ad57a..2e92afb5439 100644
--- a/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter.rb
@@ -19,6 +19,8 @@ module Gitlab
def prepare_name(name, args)
case name
+ when 'react'
+ 'award'
when 'assign'
event_name_for_assign(args)
when 'copy_metadata'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ffba34b0765..1924835a1e7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -50961,7 +50961,7 @@ msgstr ""
msgid "Toggle details"
msgstr ""
-msgid "Toggle emoji award"
+msgid "Toggle emoji reaction"
msgstr ""
msgid "Toggle file browser"
@@ -50988,10 +50988,10 @@ msgstr ""
msgid "Toggle the navigation sidebar"
msgstr ""
-msgid "Toggled :%{name}: emoji award."
+msgid "Toggled :%{name}: emoji reaction."
msgstr ""
-msgid "Toggles :%{name}: emoji award."
+msgid "Toggles :%{name}: emoji reaction."
msgstr ""
msgid "Token"
diff --git a/spec/bin/feature_flag_spec.rb b/spec/bin/feature_flag_spec.rb
index d1e4be5be28..f9caa5549ca 100644
--- a/spec/bin/feature_flag_spec.rb
+++ b/spec/bin/feature_flag_spec.rb
@@ -7,12 +7,28 @@ load File.expand_path('../../bin/feature-flag', __dir__)
RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
using RSpec::Parameterized::TableSyntax
+ let(:groups) do
+ {
+ geo: { label: 'group::geo' }
+ }
+ end
+
+ before do
+ allow(HTTParty)
+ .to receive(:get)
+ .with(FeatureFlagOptionParser::WWW_GITLAB_COM_GROUPS_JSON, format: :plain)
+ .and_return(groups.to_json)
+ end
+
describe FeatureFlagCreator do
- let(:argv) { %w[feature-flag-name -t development -g group::geo -i https://url -m http://url] }
+ let(:argv) { %w[feature-flag-name -t gitlab_com_derisk -g group::geo -a https://url -i https://url -m http://url -u username -M 16.6] }
let(:options) { FeatureFlagOptionParser.parse(argv) }
let(:creator) { described_class.new(options) }
let(:existing_flags) do
- { 'existing_feature_flag' => File.join('config', 'feature_flags', 'development', 'existing_feature_flag.yml') }
+ {
+ 'existing_feature_flag' =>
+ File.join('config', 'feature_flags', 'gitlab_com_derisk', 'existing_feature_flag.yml')
+ }
end
before do
@@ -31,7 +47,7 @@ RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
it 'properly creates a feature flag' do
expect(File).to receive(:write).with(
- File.join('config', 'feature_flags', 'development', 'feature_flag_name.yml'),
+ File.join('config', 'feature_flags', 'gitlab_com_derisk', 'feature_flag_name.yml'),
anything)
expect do
@@ -108,85 +124,97 @@ RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
end
describe '.read_type' do
- let(:type) { 'development' }
+ before do
+ stub_const('FeatureFlagOptionParser::TYPES',
+ development: { description: 'short' },
+ deprecated: { description: 'deprecated', deprecated: true },
+ licensed: { description: 'licensed' }
+ )
+ end
- context 'when there is only a single type defined' do
- before do
- stub_const('FeatureFlagOptionParser::TYPES',
- development: { description: 'short' }
- )
- end
+ context 'when valid type is given' do
+ let(:type) { 'development' }
- it 'returns that type' do
- expect(described_class.read_type).to eq(:development)
+ it 'reads type from stdin' do
+ expect(Readline).to receive(:readline).and_return(type)
+ expect do
+ expect(described_class.read_type).to eq(:development)
+ end.to output(/Specify the feature flag type/).to_stdout
end
end
- context 'when there is deprecated feature flag type' do
- before do
- stub_const('FeatureFlagOptionParser::TYPES',
- development: { description: 'short' },
- deprecated: { description: 'deprecated', deprecated: true }
- )
+ context 'when valid index is given' do
+ it 'picks the type successfully' do
+ expect(Readline).to receive(:readline).and_return('3')
+ expect do
+ expect(described_class.read_type).to eq(:licensed)
+ end.to output(/Specify the feature flag type./).to_stdout
end
+ end
- context 'and deprecated type is given' do
- let(:type) { 'deprecated' }
+ context 'when deprecated type is given' do
+ let(:type) { 'deprecated' }
- it 'shows error message and retries' do
- expect(Readline).to receive(:readline).and_return(type)
- expect(Readline).to receive(:readline).and_raise('EOF')
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
- expect do
- expect { described_class.read_type }.to raise_error(/EOF/)
- end.to output(/Specify the feature flag type/).to_stdout
- .and output(/Invalid type specified/).to_stderr
- end
+ expect do
+ expect { described_class.read_type }.to raise_error(/EOF/)
+ end.to output(/Specify the feature flag type/).to_stdout
+ .and output(/Invalid type specified/).to_stderr
end
end
- context 'when there are many types defined' do
- before do
- stub_const('FeatureFlagOptionParser::TYPES',
- development: { description: 'short' },
- licensed: { description: 'licensed' }
- )
- end
+ context 'when invalid type is given' do
+ let(:type) { 'invalid' }
- it 'reads type from stdin' do
+ it 'shows error message and retries' do
expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
expect do
- expect(described_class.read_type).to eq(:development)
+ expect { described_class.read_type }.to raise_error(/EOF/)
end.to output(/Specify the feature flag type/).to_stdout
+ .and output(/Invalid type specified/).to_stderr
end
+ end
- context 'when invalid type is given' do
- let(:type) { 'invalid' }
-
- it 'shows error message and retries' do
- expect(Readline).to receive(:readline).and_return(type)
- expect(Readline).to receive(:readline).and_raise('EOF')
+ context 'when invalid index is given' do
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return('12')
+ expect(Readline).to receive(:readline).and_raise('EOF')
- expect do
- expect { described_class.read_type }.to raise_error(/EOF/)
- end.to output(/Specify the feature flag type/).to_stdout
- .and output(/Invalid type specified/).to_stderr
- end
+ expect do
+ expect { described_class.read_type }.to raise_error(/EOF/)
+ end.to output(/Specify the feature flag type/).to_stdout
+ .and output(/Invalid type specified/).to_stderr
end
end
end
describe '.read_group' do
- let(:group) { 'group::geo' }
+ context 'when valid group is given' do
+ let(:group) { 'group::geo' }
- it 'reads type from stdin' do
- expect(Readline).to receive(:readline).and_return(group)
- expect do
- expect(described_class.read_group).to eq('group::geo')
- end.to output(/Specify the group introducing the feature flag/).to_stdout
+ it 'reads group from stdin' do
+ expect(Readline).to receive(:readline).and_return(group)
+ expect do
+ expect(described_class.read_group).to eq('group::geo')
+ end.to output(/Specify the group label to which the feature flag belongs, from the following list/).to_stdout
+ end
end
- context 'invalid group given' do
+ context 'when valid index is given' do
+ it 'picks the group successfully' do
+ expect(Readline).to receive(:readline).and_return('1')
+ expect do
+ expect(described_class.read_group).to eq('group::geo')
+ end.to output(/Specify the group label to which the feature flag belongs, from the following list/).to_stdout
+ end
+ end
+
+ context 'with invalid group given' do
let(:type) { 'invalid' }
it 'shows error message and retries' do
@@ -195,78 +223,151 @@ RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
expect do
expect { described_class.read_group }.to raise_error(/EOF/)
- end.to output(/Specify the group introducing the feature flag/).to_stdout
- .and output(/The group needs to include/).to_stderr
+ end.to output(/Specify the group label to which the feature flag belongs, from the following list/).to_stdout
+ .and output(/The group label isn't in the above labels list/).to_stderr
+ end
+ end
+
+ context 'when invalid index is given' do
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return('12')
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_group }.to raise_error(/EOF/)
+ end.to output(/Specify the group label to which the feature flag belongs, from the following list/).to_stdout
+ .and output(/The group label isn't in the above labels list/).to_stderr
end
end
end
- describe '.read_introduced_by_url' do
- let(:url) { 'https://merge-request' }
+ shared_examples 'read_url' do |method, prompt|
+ context 'with valid URL given' do
+ let(:url) { 'https://merge-request' }
- it 'reads type from stdin' do
- expect(Readline).to receive(:readline).and_return(url)
- expect do
- expect(described_class.read_introduced_by_url).to eq('https://merge-request')
- end.to output(/URL of the MR introducing the feature flag/).to_stdout
+ it 'reads URL from stdin' do
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(HTTParty).to receive(:head).with(url).and_return(instance_double(HTTParty::Response, success?: true))
+
+ expect do
+ expect(described_class.public_send(method)).to eq('https://merge-request')
+ end.to output(/#{prompt}/).to_stdout
+ end
end
- context 'empty URL given' do
+ context 'with invalid URL given' do
+ let(:url) { 'https://invalid' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(HTTParty).to receive(:head).with(url).and_return(instance_double(HTTParty::Response, success?: false))
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.public_send(method) }.to raise_error(/EOF/)
+ end.to output(/#{prompt}/).to_stdout
+ .and output(/URL '#{url}' isn't valid/).to_stderr
+ end
+ end
+
+ context 'with empty URL given' do
let(:url) { '' }
it 'skips entry' do
expect(Readline).to receive(:readline).and_return(url)
+
expect do
- expect(described_class.read_introduced_by_url).to be_nil
- end.to output(/URL of the MR introducing the feature flag/).to_stdout
+ expect(described_class.public_send(method)).to be_nil
+ end.to output(/#{prompt}/).to_stdout
end
end
- context 'invalid URL given' do
- let(:url) { 'invalid' }
+ context 'with a non-URL given' do
+ let(:url) { 'malformed' }
it 'shows error message and retries' do
expect(Readline).to receive(:readline).and_return(url)
expect(Readline).to receive(:readline).and_raise('EOF')
expect do
- expect { described_class.read_introduced_by_url }.to raise_error(/EOF/)
- end.to output(/URL of the MR introducing the feature flag/).to_stdout
+ expect { described_class.public_send(method) }.to raise_error(/EOF/)
+ end.to output(/#{prompt}/).to_stdout
.and output(/URL needs to start with/).to_stderr
end
end
end
+ describe '.read_feature_issue_url' do
+ it_behaves_like 'read_url', :read_feature_issue_url, 'URL of the original feature issue'
+ end
+
+ describe '.read_introduced_by_url' do
+ it_behaves_like 'read_url', :read_introduced_by_url, 'URL of the MR introducing the feature flag'
+ end
+
describe '.read_rollout_issue_url' do
- let(:options) { double('options', name: 'foo', type: :development) }
- let(:url) { 'https://issue' }
+ let(:options) do
+ FeatureFlagOptionParser::Options.new({
+ name: 'foo',
+ username: 'joe',
+ type: :gitlab_com_derisk,
+ introduced_by_url: 'https://introduced_by_url',
+ feature_issue_url: 'https://feature_issue_url',
+ milestone: '16.6',
+ group: 'group::geo'
+ })
+ end
- it 'reads type from stdin' do
- expect(Readline).to receive(:readline).and_return(url)
- expect do
- expect(described_class.read_rollout_issue_url(options)).to eq('https://issue')
- end.to output(/URL of the rollout issue/).to_stdout
+ context 'with valid URL given' do
+ let(:url) { 'https://rollout_issue_url' }
+
+ it 'reads type from stdin' do
+ expect(described_class).to receive(:copy_to_clipboard!).and_return(true)
+ expect(Readline).to receive(:readline).and_return('') # enter to open the new issue url
+ expect(described_class).to receive(:open_url!).and_return(true)
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(HTTParty).to receive(:head).with(url).and_return(instance_double(HTTParty::Response, success?: true))
+
+ expect do
+ expect(described_class.read_rollout_issue_url(options)).to eq(url)
+ end.to output(/URL of the rollout issue/).to_stdout
+ end
end
- context 'invalid URL given' do
- let(:type) { 'invalid' }
+ context 'with invalid URL given' do
+ let(:url) { 'https://invalid' }
it 'shows error message and retries' do
- expect(Readline).to receive(:readline).and_return(type)
+ expect(described_class).to receive(:copy_to_clipboard!).and_return(true)
+ expect(Readline).to receive(:readline).and_return('') # enter to open the new issue url
+ expect(described_class).to receive(:open_url!).and_return(true)
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(HTTParty).to receive(:head).with(url).and_return(instance_double(HTTParty::Response, success?: false))
expect(Readline).to receive(:readline).and_raise('EOF')
expect do
expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/)
end.to output(/URL of the rollout issue/).to_stdout
- .and output(/URL needs to start/).to_stderr
+ .and output(/URL '#{url}' isn't valid/).to_stderr
end
end
- end
- describe '.read_ee_only' do
- let(:options) { double('options', name: 'foo', type: :development) }
+ context 'with a non-URL given' do
+ let(:url) { 'malformed' }
+
+ it 'shows error message and retries' do
+ expect(described_class).to receive(:copy_to_clipboard!).and_return(true)
+ expect(Readline).to receive(:readline).and_return('') # enter to open the new issue url
+ expect(described_class).to receive(:open_url!).and_return(true)
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(Readline).to receive(:readline).and_raise('EOF')
- it { expect(described_class.read_ee_only(options)).to eq(false) }
+ expect do
+ expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/)
+ end.to output(/URL of the rollout issue/).to_stdout
+ .and output(/URL needs to start/).to_stderr
+ end
+ end
end
end
end
diff --git a/spec/finders/packages/terraform_module/packages_finder_spec.rb b/spec/finders/packages/terraform_module/packages_finder_spec.rb
new file mode 100644
index 00000000000..4550b3be055
--- /dev/null
+++ b/spec/finders/packages/terraform_module/packages_finder_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Packages::TerraformModule::PackagesFinder, feature_category: :package_registry do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package1) { create(:terraform_module_package, project: project, version: '1.0.0') }
+ let_it_be(:package2) { create(:terraform_module_package, project: project, version: '2.0.0', name: package1.name) }
+
+ let(:params) { {} }
+
+ subject { described_class.new(project, params).execute }
+
+ describe '#execute' do
+ context 'without project' do
+ let(:project) { nil }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'without package_name' do
+ let(:params) { { package_name: nil } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with package_name' do
+ let(:params) { { package_name: package1.name } }
+
+ it 'returns packages with the given name ordered by version desc' do
+ is_expected.to eq([package2, package1])
+ end
+
+ context 'with package_version' do
+ let(:params) { { package_name: package1.name, package_version: package1.version } }
+
+ it { is_expected.to eq([package1]) }
+ end
+
+ context 'when package is not installable' do
+ before do
+ package1.update_column(:status, 3)
+ end
+
+ it { is_expected.to eq([package2]) }
+ end
+
+ context 'when package has no version' do
+ before do
+ package1.update_column(:version, nil)
+ end
+
+ it { is_expected.to eq([package2]) }
+ end
+
+ context 'when package is not a terraform module' do
+ before do
+ package1.update_column(:package_type, 1)
+ end
+
+ it { is_expected.to eq([package2]) }
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb
index 9e1a2bfd466..d80235023ef 100644
--- a/spec/graphql/types/namespace_type_spec.rb
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -15,5 +15,5 @@ RSpec.describe GitlabSchema.types['Namespace'] do
expect(described_class).to include_graphql_fields(*expected_fields)
end
- specify { expect(described_class).to require_graphql_authorizations(:read_namespace_via_membership) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_namespace) }
end
diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb
index 595725d357c..b75c780a33e 100644
--- a/spec/lib/feature/definition_spec.rb
+++ b/spec/lib/feature/definition_spec.rb
@@ -30,11 +30,11 @@ RSpec.describe Feature::Definition do
:name | 'ALL_CAPS' | /Feature flag 'ALL_CAPS' is invalid/
: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 | 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/
+ :path | 'invalid/feature_flag.yml' | /Feature flag 'feature_flag' has an invalid path/
+ :default_enabled | nil | /Feature flag 'feature_flag' is missing `default_enabled`/
end
with_them do
diff --git a/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
index 1940442d2ad..903ae64cf33 100644
--- a/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
@@ -31,6 +31,14 @@ RSpec.describe Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter, :cle
end
end
+ context 'when tracking react' do
+ let(:quickaction_name) { 'react' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_award' }
+ end
+ end
+
context 'tracking assigns' do
let(:quickaction_name) { 'assign' }
diff --git a/spec/models/work_items/hierarchy_restriction_spec.rb b/spec/models/work_items/hierarchy_restriction_spec.rb
index 2c4d5d32fb8..890c007b6cd 100644
--- a/spec/models/work_items/hierarchy_restriction_spec.rb
+++ b/spec/models/work_items/hierarchy_restriction_spec.rb
@@ -15,4 +15,24 @@ RSpec.describe WorkItems::HierarchyRestriction do
it { is_expected.to validate_presence_of(:child_type) }
it { is_expected.to validate_uniqueness_of(:child_type).scoped_to(:parent_type_id) }
end
+
+ describe '#clear_parent_type_cache!' do
+ subject(:hierarchy_restriction) { build(:hierarchy_restriction) }
+
+ context 'when a hierarchy restriction is saved' do
+ it 'calls #clear_reactive_cache! on parent type' do
+ expect(hierarchy_restriction.parent_type).to receive(:clear_reactive_cache!).once
+
+ hierarchy_restriction.save!
+ end
+ end
+
+ context 'when a hierarchy restriction is destroyed' do
+ it 'calls #clear_reactive_cache! on parent type' do
+ expect(hierarchy_restriction.parent_type).to receive(:clear_reactive_cache!).once
+
+ hierarchy_restriction.destroy!
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index a4bc94798be..107fcd8dcdd 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'getting projects', feature_category: :groups_and_projects do
'namespace',
{ 'fullPath' => subject.full_path },
<<~QUERY
+ id
projects(includeSubgroups: #{include_subgroups}) {
edges {
node {
@@ -53,24 +54,30 @@ RSpec.describe 'getting projects', feature_category: :groups_and_projects do
expect(graphql_data['namespace']['projects']['edges'].size).to eq(count)
end
+ end
- context 'with no user' do
- it 'finds only public projects' do
- post_graphql(query, current_user: nil)
+ it_behaves_like 'a graphql namespace'
- expect(graphql_data['namespace']).to be_nil
- end
+ context 'when no user is given' do
+ it 'finds only public projects' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data_at(:namespace, :projects, :edges).size).to eq(1)
end
end
- it_behaves_like 'a graphql namespace'
-
context 'when the namespace is a user' do
subject { user.namespace }
let(:include_subgroups) { false }
it_behaves_like 'a graphql namespace'
+
+ it 'does not show namespace entity for anonymous user' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['namespace']).to be_nil
+ end
end
context 'when not including subgroups' do
diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
index c8819f1e38f..273b6b8c25b 100644
--- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe 'rendering namespace statistics', feature_category: :metrics do
it 'hides statistics for unauthenticated requests' do
post_graphql(query, current_user: nil)
- expect(graphql_data['namespace']).to be_blank
+ expect(graphql_data_at(:namespace, :root_storage_statistics)).to be_blank
end
end
end
diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb
index c0c7c5fee2b..86808915564 100644
--- a/spec/requests/api/graphql/namespace_query_spec.rb
+++ b/spec/requests/api/graphql/namespace_query_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe 'Query', feature_category: :groups_and_projects do
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
- let_it_be(:group_namespace) { create(:group) }
+ let_it_be(:group_namespace) { create(:group, :private) }
+ let_it_be(:public_group_namespace) { create(:group, :public) }
let_it_be(:user_namespace) { create(:user_namespace, owner: user) }
let_it_be(:project_namespace) { create(:project_namespace, parent: group_namespace) }
@@ -60,6 +61,51 @@ RSpec.describe 'Query', feature_category: :groups_and_projects do
end
end
+ context 'when used with a public group' do
+ let(:target_namespace) { public_group_namespace }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ context 'when user is a member' do
+ before do
+ public_group_namespace.add_developer(user)
+ end
+
+ it 'fetches the expected data' do
+ expect(query_result).to include(
+ 'fullPath' => target_namespace.full_path,
+ 'name' => target_namespace.name
+ )
+ end
+ end
+
+ context 'when user is anonymous' do
+ let(:current_user) { nil }
+
+ it 'fetches the expected data' do
+ expect(query_result).to include(
+ 'fullPath' => target_namespace.full_path,
+ 'name' => target_namespace.name
+ )
+ end
+ end
+
+ context 'when user is not a member' do
+ let(:current_user) { other_user }
+
+ it 'fetches the expected data' do
+ expect(query_result).to include(
+ 'fullPath' => target_namespace.full_path,
+ 'name' => target_namespace.name
+ )
+ end
+ end
+ end
+
it_behaves_like 'retrieving a namespace' do
let(:target_namespace) { group_namespace }
diff --git a/spec/requests/api/terraform/modules/v1/project_packages_spec.rb b/spec/requests/api/terraform/modules/v1/project_packages_spec.rb
index 1f3b2283d59..3377f8d6647 100644
--- a/spec/requests/api/terraform/modules/v1/project_packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/project_packages_spec.rb
@@ -6,6 +6,18 @@ RSpec.describe API::Terraform::Modules::V1::ProjectPackages, feature_category: :
include_context 'for terraform modules api setup'
using RSpec::Parameterized::TableSyntax
+ describe 'GET /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system' do
+ it_behaves_like 'handling project level terraform module download requests' do
+ let(:module_version) { nil }
+ end
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version' do
+ it_behaves_like 'handling project level terraform module download requests' do
+ let(:module_version) { package.version }
+ end
+ end
+
describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do
include_context 'workhorse headers'
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 63b5d54a18d..0e46391c0ad 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -188,28 +188,4 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService, feature_catego
service.execute
end
end
-
- context 'when the domain URL is longer than 64 characters' do
- let(:long_domain) { "a.b.c.#{'d' * 63}" }
- let(:pages_domain) { create(:pages_domain, :without_certificate, :without_key, domain: long_domain) }
- let(:service) { described_class.new(pages_domain) }
-
- it 'logs an error and does not proceed with certificate acquisition' do
- expect(Gitlab::AppLogger).to receive(:error).with(
- hash_including(
- message: "Domain name too long for Let's Encrypt certificate",
- pages_domain: long_domain,
- pages_domain_bytesize: long_domain.bytesize,
- max_allowed_bytesize: described_class::MAX_DOMAIN_LENGTH,
- project_id: pages_domain.project_id
- )
- )
-
- # Ensure that the certificate acquisition is not attempted
- expect(::PagesDomains::CreateAcmeOrderService).not_to receive(:new)
- expect(PagesDomainSslRenewalWorker).not_to receive(:perform_in)
-
- service.execute
- end
- end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index dc93fd96aee..c1dbb5b80b3 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -564,7 +564,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'returns the reaction message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq('Toggled :100: emoji award.')
+ expect(message).to eq('Toggled :100: emoji reaction.')
end
end
@@ -1911,8 +1911,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
let(:content) { "#{command} :100:" }
let(:issuable) { commit }
- # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/434446
- it_behaves_like 'failed command', "Could not apply award command."
+ it_behaves_like 'failed command', "Could not apply react command."
end
end
end
@@ -2877,7 +2876,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'includes the emoji' do
_, explanations = service.explain(content, issue)
- expect(explanations).to eq(['Toggles :confetti_ball: emoji award.'])
+ expect(explanations).to eq(['Toggles :confetti_ball: emoji reaction.'])
end
end
diff --git a/spec/support/formatters/json_formatter.rb b/spec/support/formatters/json_formatter.rb
index a54004b3024..398ff0187a1 100644
--- a/spec/support/formatters/json_formatter.rb
+++ b/spec/support/formatters/json_formatter.rb
@@ -89,7 +89,13 @@ module Support
[metadata[:file_path], metadata[:line_number]]
else
# If there are nested shared examples, the outermost location is last in the array
- metadata[:shared_group_inclusion_backtrace].last.formatted_inclusion_location.split(':')
+ (
+ metadata[:shared_group_inclusion_backtrace].last.formatted_inclusion_location ||
+ # RSpec ignores some paths by default, e.g. bin/, which result in the above being nil.
+ # Source: https://github.com/rspec/rspec-core/blob/v3.12.2/lib/rspec/core/backtrace_formatter.rb#L11
+ # In that case, we fallback to use the raw `#inclusion_location`.
+ metadata[:shared_group_inclusion_backtrace].last.inclusion_location
+ ).split(':')
end
end
diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
index ae2855083f6..b56de050d1e 100644
--- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
@@ -150,8 +150,15 @@ RSpec.shared_examples 'grants terraform module package file access' do |user_typ
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
- it_behaves_like 'returning response status', status
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/octet-stream')
+ expect(response.body).to eq(package.package_files.last.file.read)
+ end
end
end
@@ -273,3 +280,169 @@ RSpec.shared_examples 'process terraform module upload' do |user_type, status, a
end
end
end
+
+RSpec.shared_examples 'handling project level terraform module download requests' do
+ using RSpec::Parameterized::TableSyntax
+ let(:project_id) { project.id }
+ let(:package_name) { package.name }
+ let(:url) { "/projects/#{project_id}/packages/terraform/modules/#{package_name}/#{module_version}?archive=tgz" }
+
+ subject { get api(url), headers: headers }
+
+ it { is_expected.to have_request_urgency(:low) }
+
+ context 'with valid project' do
+ where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do
+ :public | :anonymous | false | nil | 'grants terraform module package file access' | :success
+ :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized
+
+ :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :internal | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :internal | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :internal | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+ :internal | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
+
+ :public | :developer | true | :personal_access_token | 'grants terraform module package file access' | :success
+ :public | :guest | true | :personal_access_token | 'grants terraform module package file access' | :success
+ :public | :developer | false | :personal_access_token | 'grants terraform module package file access' | :success
+ :public | :guest | false | :personal_access_token | 'grants terraform module package file access' | :success
+ :private | :developer | true | :personal_access_token | 'grants terraform module package file access' | :success
+ :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :not_found
+ :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :not_found
+ :internal | :developer | true | :personal_access_token | 'grants terraform module package file access' | :success
+ :internal | :guest | true | :personal_access_token | 'grants terraform module package file access' | :success
+ :internal | :developer | false | :personal_access_token | 'grants terraform module package file access' | :success
+ :internal | :guest | false | :personal_access_token | 'grants terraform module package file access' | :success
+
+ :public | :developer | true | :job_token | 'grants terraform module package file access' | :success
+ :public | :guest | true | :job_token | 'grants terraform module package file access' | :success
+ :public | :developer | false | :job_token | 'grants terraform module package file access' | :success
+ :public | :guest | false | :job_token | 'grants terraform module package file access' | :success
+ :private | :developer | true | :job_token | 'grants terraform module package file access' | :success
+ :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden
+ :private | :developer | false | :job_token | 'rejects terraform module packages access' | :not_found
+ :private | :guest | false | :job_token | 'rejects terraform module packages access' | :not_found
+ :internal | :developer | true | :job_token | 'grants terraform module package file access' | :success
+ :internal | :guest | true | :job_token | 'grants terraform module package file access' | :success
+ :internal | :developer | false | :job_token | 'grants terraform module package file access' | :success
+ :internal | :guest | false | :job_token | 'grants terraform module package file access' | :success
+
+ :public | :anonymous | false | :deploy_token | 'grants terraform module package file access' | :success
+ :private | :anonymous | false | :deploy_token | 'grants terraform module package file access' | :success
+ :internal | :anonymous | false | :deploy_token | 'grants terraform module package file access' | :success
+ end
+
+ with_them do
+ let(:headers) do
+ case token_type
+ when :personal_access_token, :invalid
+ basic_auth_headers(user.username, token)
+ when :deploy_token
+ basic_auth_headers(deploy_token.username, token)
+ when :job_token
+ basic_auth_headers(::Gitlab::Auth::CI_JOB_USER, token)
+ else
+ {}
+ end
+ end
+
+ let(:snowplow_gitlab_standard_context) do
+ {
+ project: project,
+ namespace: project.namespace,
+ property: 'i_package_terraform_module_user'
+ }.tap do |context|
+ context[:user] = user if token_type && token_type != :deploy_token
+ context[:user] = deploy_token if token_type == :deploy_token
+ end
+ end
+
+ before do
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ context 'with/without module version' do
+ let(:headers) { basic_auth_headers }
+ let(:finder_params) do
+ { package_name: package_name }.tap do |p|
+ p[:package_version] = module_version if module_version
+ end
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'calls the finder with the correct params' do
+ expect_next_instance_of(::Packages::TerraformModule::PackagesFinder, project, finder_params) do |finder|
+ expect(finder).to receive(:execute).and_call_original
+ end
+
+ subject
+ end
+ end
+
+ context 'with non-existent module version' do
+ let(:headers) { basic_auth_headers }
+ let(:module_version) { '1.99.322' }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ context 'with invalid project' do
+ let(:project_id) { '123456' }
+
+ let(:headers) { basic_auth_headers }
+
+ it_behaves_like 'rejects terraform module packages access', :anonymous, :not_found
+ end
+
+ context 'with invalid package name' do
+ let(:headers) { basic_auth_headers }
+
+ [nil, '', '%20', 'unknown', '..%2F..', '../..'].each do |pkg_name|
+ context "with package name #{pkg_name}" do
+ let(:package_name) { pkg_name }
+
+ it_behaves_like 'rejects terraform module packages access', :anonymous, :not_found
+ end
+ end
+ end
+
+ context 'when terraform-get param is received' do
+ let(:headers) { basic_auth_headers }
+ let(:url) { "#{super().split('?').first}?terraform-get=1" }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.headers).to include 'X-Terraform-Get'
+ expect(response.headers['X-Terraform-Get']).to include '?archive=tgz'
+ expect(response.headers['X-Terraform-Get']).not_to include 'terraform-get=1'
+ end
+ end
+
+ def basic_auth_headers(username = user.username, password = personal_access_token.token)
+ { Authorization: "Basic #{Base64.strict_encode64("#{username}:#{password}")}" }
+ end
+end
diff --git a/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
index 0545be7c741..4f6b27a99c6 100644
--- a/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
+++ b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
@@ -7,12 +7,23 @@ RSpec.shared_examples 'work item hierarchy restrictions importer' do
end
end
+ shared_examples 'clears type reactive cache' do
+ specify do
+ expect_next_found_instances_of(WorkItems::Type, 7) do |instance|
+ expect(instance).to receive(:clear_reactive_cache!)
+ end
+
+ subject
+ end
+ end
+
context 'when restrictions are missing' do
before do
WorkItems::HierarchyRestriction.delete_all
end
it_behaves_like 'adds restrictions'
+ it_behaves_like 'clears type reactive cache'
end
context 'when base types are missing' do
@@ -41,6 +52,8 @@ RSpec.shared_examples 'work item hierarchy restrictions importer' do
change { restriction.maximum_depth }.from(depth + 1).to(depth)
)
end
+
+ it_behaves_like 'clears type reactive cache'
end
context 'when some restrictions are missing' do
@@ -55,6 +68,8 @@ RSpec.shared_examples 'work item hierarchy restrictions importer' do
)
expect(WorkItems::HierarchyRestriction.count).to eq(7)
end
+
+ it_behaves_like 'clears type reactive cache'
end
context 'when restrictions contain attributes not present in the table' do
@@ -70,5 +85,7 @@ RSpec.shared_examples 'work item hierarchy restrictions importer' do
subject
end
+
+ it_behaves_like 'clears type reactive cache'
end
end