Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-12-19 09:10:52 +00:00
parent 6c2b987064
commit 17295c75a1
43 changed files with 1276 additions and 372 deletions

View File

@ -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

View File

@ -1,6 +1,4 @@
html {
overflow-y: scroll;
&.touch .tooltip {
display: none !important;
}

View File

@ -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

View File

@ -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.'

View File

@ -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'

View File

@ -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

View File

@ -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",

View File

@ -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 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
# if there's only one type, do not ask, return
return TYPES.first.first if TYPES.one?
$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]
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]
$stderr.puts "Invalid type specified '#{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_group
$stdout.puts
$stdout.puts ">> Specify the group label to which the feature flag belongs, from the following list:\n#{group_list}"
loop do
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
end
end
def read_feature_issue_url
read_url('URL of the original feature issue (enter to skip):')
end
def read_introduced_by_url
$stdout.puts
$stdout.puts ">> URL of the MR introducing the feature flag (enter to skip):"
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://')
$stderr.puts "URL needs to start with https://"
end
end
def read_ee_only(options)
TYPES.dig(options.type, :ee_only)
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
70f8264aa0996e3020fd068beba148f51170224126d1aa91740bd32bd59a196d

View File

@ -0,0 +1 @@
281fde69710c20f9ae845136a4bfdbce1a8396f3d3d17018f7ffce1bf230b888

View File

@ -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);

View File

@ -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:

View File

@ -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. |

View File

@ -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

View File

@ -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
`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.
`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.
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).
#### 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)
#### Usage
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)
```
For `development` feature flags, the type doesn't need to be specified (they're the default type).
### `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)
```
### `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.
To invoke `ops` feature flags, you must append `type: :ops`:
#### 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
#### 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`.

View File

@ -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:

View File

@ -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:

View File

@ -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**.

View File

@ -16,87 +16,174 @@ module API
require_packages_enabled!
end
helpers do
params :terraform_get do
optional 'terraform-get', type: String, values: %w[1], desc: 'Terraform get redirection flag'
end
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
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
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
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/*module_version/file' do
namespace ':id/packages/terraform/modules/:module_name/:module_system' 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)
accept.token_types(
:personal_access_token_with_username,
:deploy_token_with_username,
:job_token_with_username
).sent_through(:http_basic_auth)
end
desc 'Workhorse authorize Terraform Module package file' do
detail 'This feature was introduced in GitLab 13.11'
success code: 200
desc 'Download the latest version of a module' do
detail 'This feature was introduced in GitLab 16.7'
success code: 204
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
use :terraform_get
end
get do
present_package_file
end
params do
requires :file, type: ::API::Validations::Types::WorkhorseFile,
desc: 'The package file to be published (generated by Multipart middleware)',
documentation: { type: 'file' }
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
put do
authorize_upload!(authorized_user_project)
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
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(
:terraform_module_max_file_size, params[:file].size)
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
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
}
put :authorize do
authorize_workhorse!(
subject: authorized_user_project,
maximum_size: authorized_user_project.actual_limits.terraform_module_max_file_size
)
end
result = ::Packages::TerraformModule::CreatePackageService
.new(authorized_user_project, current_user, create_package_file_params)
.execute
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
render_api_error!(result[:message], result[:http_status]) if result[:status] == :error
params do
requires :file, type: ::API::Validations::Types::WorkhorseFile,
desc: 'The package file to be published (generated by Multipart middleware)',
documentation: { type: 'file' }
end
track_package_event('push_package', :terraform_module, project: authorized_user_project,
namespace: authorized_user_project.namespace)
put do
authorize_upload!(authorized_user_project)
created!
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(
e,
extra: { file_name: params[:file_name], project_id: authorized_user_project.id }
)
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(
:terraform_module_max_file_size, params[:file].size
)
forbidden!
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

View File

@ -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}"
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}"
end
if default_enabled.nil?
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing default_enabled. Ensure to update #{path}"
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

View File

@ -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,
ee_only: false,
default_enabled: false,
can_be_default_enabled: false,
example: <<-EOS
Feature.enabled?(:my_feature_flag, project)
Feature.enabled?(:my_feature_flag, project, type: :development)
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,
can_be_default_enabled: true,
example: <<-EOS
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

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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"

View File

@ -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,50 +124,16 @@ RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
end
describe '.read_type' do
let(:type) { 'development' }
context 'when there is only a single type defined' do
before do
stub_const('FeatureFlagOptionParser::TYPES',
development: { description: 'short' }
)
end
it 'returns that type' do
expect(described_class.read_type).to eq(:development)
end
before do
stub_const('FeatureFlagOptionParser::TYPES',
development: { description: 'short' },
deprecated: { description: 'deprecated', deprecated: true },
licensed: { description: 'licensed' }
)
end
context 'when there is deprecated feature flag type' do
before do
stub_const('FeatureFlagOptionParser::TYPES',
development: { description: 'short' },
deprecated: { description: 'deprecated', deprecated: true }
)
end
context 'and 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')
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
context 'when there are many types defined' do
before do
stub_const('FeatureFlagOptionParser::TYPES',
development: { description: 'short' },
licensed: { description: 'licensed' }
)
end
context 'when valid type is given' do
let(:type) { 'development' }
it 'reads type from stdin' do
expect(Readline).to receive(:readline).and_return(type)
@ -159,34 +141,80 @@ RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
expect(described_class.read_type).to eq(:development)
end.to output(/Specify the feature flag type/).to_stdout
end
end
context 'when invalid type is given' do
let(:type) { 'invalid' }
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
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 deprecated type is given' do
let(:type) { 'deprecated' }
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
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
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')
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 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
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,64 +223,143 @@ 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_rollout_issue_url' do
let(:options) { double('options', name: 'foo', type: :development) }
let(:url) { 'https://issue' }
describe '.read_feature_issue_url' do
it_behaves_like 'read_url', :read_feature_issue_url, 'URL of the original feature issue'
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
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) 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
context 'invalid URL given' do
let(:type) { 'invalid' }
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 '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 '#{url}' isn't valid/).to_stderr
end
end
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')
expect do
@ -262,11 +369,5 @@ RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
end
end
end
describe '.read_ee_only' do
let(:options) { double('options', name: 'foo', type: :development) }
it { expect(described_class.read_ee_only(options)).to eq(false) }
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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

View File

@ -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
context 'with no user' do
it 'finds only public projects' do
post_graphql(query, current_user: nil)
expect(graphql_data['namespace']).to be_nil
end
end
end
it_behaves_like 'a graphql namespace'
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
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

View File

@ -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

View File

@ -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 }

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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