Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2d8c28f1d3
commit
9fdb3dbd6b
|
|
@ -169,6 +169,7 @@ Naming/FileName:
|
|||
- 'qa/tasks/**/*.rake'
|
||||
- '**/*.ru'
|
||||
- 'app/graphql/types/issue_connection.rb'
|
||||
- 'app/graphql/types/group_connection.rb'
|
||||
|
||||
IgnoreExecutableScripts: true
|
||||
AllowedAcronyms:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
c2dd35e7775018bd29c2b1306afeee966ba3b4d7
|
||||
cf32d208912de9dfbfdd4baab42655baf82bfce5
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -353,7 +353,7 @@ gem 'snowplow-tracker', '~> 0.6.1'
|
|||
|
||||
# Metrics
|
||||
gem 'webrick', '~> 1.6.1', require: false
|
||||
gem 'prometheus-client-mmap', '~> 0.16', require: 'prometheus/client'
|
||||
gem 'prometheus-client-mmap', '~> 0.17', require: 'prometheus/client'
|
||||
|
||||
gem 'warning', '~> 1.3.0'
|
||||
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@
|
|||
{"name":"premailer","version":"1.16.0","platform":"ruby","checksum":"03e4402c448e6bae13fb5f6301a8bde4f3508e1bff90ae7c0972c7be94694786"},
|
||||
{"name":"premailer-rails","version":"1.10.3","platform":"ruby","checksum":"7cdcb97027866f7a81c490c6d15ada7f39666b5f6375f0821b7e97e0483b112f"},
|
||||
{"name":"proc_to_ast","version":"0.1.0","platform":"ruby","checksum":"92a73fa66e2250a83f8589f818b0751bcf227c68f85916202df7af85082f8691"},
|
||||
{"name":"prometheus-client-mmap","version":"0.16.2","platform":"ruby","checksum":"36e7e96fdd603c2d1fed050ec71504797f3f8b2560123306ba72018ee3561165"},
|
||||
{"name":"prometheus-client-mmap","version":"0.17.0","platform":"ruby","checksum":"766d3706f7b26fed5a177843ab15b5b0dc108f9677d8bdbe0c4b5d9375c2af24"},
|
||||
{"name":"pry","version":"0.13.1","platform":"java","checksum":"9612d825e2c3bc160633b2a4fae21041126ee33f1ac8035c851417e561b2b46c"},
|
||||
{"name":"pry","version":"0.13.1","platform":"ruby","checksum":"1393918c415af46b6d09044d2b78dde92b29bc834fd85c369a950bab0826dc47"},
|
||||
{"name":"pry-byebug","version":"3.9.0","platform":"ruby","checksum":"3bba08f97fea15b89cc299f3b5136e3b85763cd18cf84960eac4fbfbeb2ede24"},
|
||||
|
|
|
|||
|
|
@ -1082,7 +1082,7 @@ GEM
|
|||
coderay
|
||||
parser
|
||||
unparser
|
||||
prometheus-client-mmap (0.16.2)
|
||||
prometheus-client-mmap (0.17.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
|
|
@ -1768,7 +1768,7 @@ DEPENDENCIES
|
|||
pg_query (~> 2.2)
|
||||
png_quantizator (~> 0.2.1)
|
||||
premailer-rails (~> 1.10.3)
|
||||
prometheus-client-mmap (~> 0.16)
|
||||
prometheus-client-mmap (~> 0.17)
|
||||
pry-byebug
|
||||
pry-rails (~> 0.3.9)
|
||||
pry-shell (~> 0.5.1)
|
||||
|
|
|
|||
|
|
@ -607,53 +607,65 @@
|
|||
"secrets": {
|
||||
"type": "object",
|
||||
"markdownDescription": "Defines secrets to be injected as environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secrets).",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "Environment variable name",
|
||||
"properties": {
|
||||
"vault": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`)"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"engine": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vault": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"markdownDescription": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`). [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsvault)"
|
||||
},
|
||||
"required": [
|
||||
"engine",
|
||||
"path",
|
||||
"field"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"vault"
|
||||
]
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"engine": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"engine",
|
||||
"path",
|
||||
"field"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"file": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Configures the secret to be stored as either a file or variable type CI/CD variable. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsfile)"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "Specifies the JWT variable that should be used to authenticate with Hashicorp Vault."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"vault"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"before_script": {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module Mutations
|
|||
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
|
||||
result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode])
|
||||
|
||||
track_usage_event(:incident_management_alert_assigned, current_user.id)
|
||||
track_alert_events('incident_management_alert_assigned', alert)
|
||||
|
||||
prepare_response(result)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Mutations
|
|||
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
|
||||
result = ::AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute
|
||||
|
||||
track_usage_event(:incident_management_alert_todo, current_user.id)
|
||||
track_alert_events('incident_management_alert_todo', alert)
|
||||
|
||||
prepare_response(result)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,6 +39,24 @@ module Mutations
|
|||
|
||||
::AlertManagement::AlertsFinder.new(current_user, project, args).execute.first
|
||||
end
|
||||
|
||||
def track_alert_events(event, alert)
|
||||
project = alert.project
|
||||
namespace = project.namespace
|
||||
track_usage_event(event, current_user.id)
|
||||
|
||||
return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace)
|
||||
|
||||
Gitlab::Tracking.event(
|
||||
self.class.to_s,
|
||||
event,
|
||||
project: project,
|
||||
namespace: namespace,
|
||||
user: current_user,
|
||||
label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
|
||||
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module Mutations
|
|||
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
|
||||
result = create_alert_issue(alert, current_user)
|
||||
|
||||
track_usage_event(:incident_management_incident_created, current_user.id)
|
||||
track_alert_events('incident_management_incident_created', alert)
|
||||
track_usage_event(:incident_management_alert_create_incident, current_user.id)
|
||||
|
||||
prepare_response(alert, result)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module Mutations
|
|||
alert = authorized_find!(project_path: project_path, iid: iid)
|
||||
result = update_status(alert, status)
|
||||
|
||||
track_usage_event(:incident_management_alert_status_changed, current_user.id)
|
||||
track_alert_events('incident_management_alert_status_changed', alert)
|
||||
|
||||
prepare_response(result)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ module Types
|
|||
field :executor_name, GraphQL::Types::String, null: true,
|
||||
description: 'Executor last advertised by the runner.',
|
||||
method: :executor_name
|
||||
field :groups, ::Types::GroupType.connection_type, null: true,
|
||||
description: 'Groups the runner is associated with. For group runners only.'
|
||||
field :groups, 'Types::GroupConnection', null: true,
|
||||
description: 'Groups the runner is associated with. For group runners only.'
|
||||
field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
|
||||
description: 'ID of the runner.'
|
||||
field :ip_address, GraphQL::Types::String, null: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Normally this wouldn't be needed and we could use
|
||||
#
|
||||
# type Types::GroupType.connection_type, null: true
|
||||
#
|
||||
# in a resolver. However we can end up with cyclic definitions.
|
||||
# Running the spec locally can result in errors like
|
||||
#
|
||||
# NameError: uninitialized constant Types::GroupType
|
||||
#
|
||||
# or other errors. To fix this, we created this file and use
|
||||
#
|
||||
# type "Types::GroupConnection", null: true
|
||||
#
|
||||
# which gives a delayed resolution, and the proper connection type.
|
||||
#
|
||||
# See gitlab/app/graphql/types/ci/runner_type.rb
|
||||
# Reference: https://github.com/rmosolgo/graphql-ruby/issues/3974#issuecomment-1084444214
|
||||
# and https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#testing-tips-and-tricks
|
||||
#
|
||||
Types::GroupConnection = Types::GroupType.connection_type
|
||||
|
|
@ -22,7 +22,7 @@ module Groups
|
|||
|
||||
# When running Observability UI in standalone mode (i.e. not backed by Observability Backend)
|
||||
# the group-id is not required. This is mostly used for local dev
|
||||
base_url = ENV['STANDALONE_OBSERVABILITY_UI'] == 'true' ? observability_url : "#{observability_url}/#{group.id}"
|
||||
base_url = ENV['STANDALONE_OBSERVABILITY_UI'] == 'true' ? observability_url : "#{observability_url}/-/#{group.id}"
|
||||
|
||||
sanitized_path = if params[:observability_path] && sanitize(params[:observability_path]) != ''
|
||||
CGI.unescapeHTML(sanitize(params[:observability_path]))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ module Users
|
|||
namespace_storage_limit_banner_alert_threshold: 12, # EE-only
|
||||
namespace_storage_limit_banner_error_threshold: 13, # EE-only
|
||||
usage_quota_trial_alert: 14, # EE-only
|
||||
preview_usage_quota_free_plan_alert: 15 # EE-only
|
||||
preview_usage_quota_free_plan_alert: 15, # EE-only
|
||||
enforcement_at_limit_alert: 16 # EE-only
|
||||
}
|
||||
|
||||
validates :group, presence: true
|
||||
|
|
|
|||
|
|
@ -7,7 +7,23 @@ module IncidentManagement
|
|||
def track_incident_action(current_user, target, action)
|
||||
return unless target.incident?
|
||||
|
||||
track_usage_event(:"incident_management_#{action}", current_user.id)
|
||||
event = "incident_management_#{action}"
|
||||
track_usage_event(event, current_user.id)
|
||||
|
||||
namespace = target.try(:namespace)
|
||||
project = target.try(:project)
|
||||
|
||||
return unless Feature.enabled?(:route_hll_to_snowplow_phase2, target.try(:namespace))
|
||||
|
||||
Gitlab::Tracking.event(
|
||||
self.class.to_s,
|
||||
event,
|
||||
project: project,
|
||||
namespace: namespace,
|
||||
user: current_user,
|
||||
label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
|
||||
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,6 +24,23 @@ module IncidentManagement
|
|||
def error_in_save(timeline_event)
|
||||
error(timeline_event.errors.full_messages.to_sentence)
|
||||
end
|
||||
|
||||
def track_timeline_event(event, project)
|
||||
namespace = project.namespace
|
||||
track_usage_event(event, user.id)
|
||||
|
||||
return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace)
|
||||
|
||||
Gitlab::Tracking.event(
|
||||
self.class.to_s,
|
||||
event,
|
||||
project: project,
|
||||
namespace: namespace,
|
||||
user: user,
|
||||
label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
|
||||
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ module IncidentManagement
|
|||
|
||||
create_timeline_event_tag_links(timeline_event, params[:timeline_event_tag_names])
|
||||
|
||||
track_usage_event(:incident_management_timeline_event_created, user.id)
|
||||
track_timeline_event("incident_management_timeline_event_created", project)
|
||||
|
||||
success(timeline_event)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module IncidentManagement
|
|||
if timeline_event.destroy
|
||||
add_system_note(incident, user)
|
||||
|
||||
track_usage_event(:incident_management_timeline_event_deleted, user.id)
|
||||
track_timeline_event('incident_management_timeline_event_deleted', project)
|
||||
success(timeline_event)
|
||||
else
|
||||
error_in_save(timeline_event)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ module IncidentManagement
|
|||
if timeline_event_saved
|
||||
add_system_note(timeline_event)
|
||||
|
||||
track_usage_event(:incident_management_timeline_event_edited, user.id)
|
||||
track_timeline_event('incident_management_timeline_event_edited', timeline_event.project)
|
||||
success(timeline_event)
|
||||
else
|
||||
error_in_save(timeline_event)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,11 @@ module Issues
|
|||
|
||||
Milestones::IssuesCountService.new(milestone).delete_cache
|
||||
end
|
||||
|
||||
override :allowed_create_params
|
||||
def allowed_create_params(params)
|
||||
super(params).except(:issue_type, :work_item_type_id, :work_item_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ module Issues
|
|||
# don't enqueue immediately to prevent todos removal in case of a mistake
|
||||
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
|
||||
create_confidentiality_note(issue)
|
||||
track_usage_event(:incident_management_incident_change_confidential, current_user.id)
|
||||
track_incident_action(current_user, issue, :incident_change_confidential)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -166,8 +166,9 @@ class TodoService
|
|||
|
||||
# When user marks a target as todo
|
||||
def mark_todo(target, current_user)
|
||||
attributes = attributes_for_todo(target.project, target, current_user, Todo::MARKED)
|
||||
create_todos(current_user, attributes)
|
||||
project = target.project
|
||||
attributes = attributes_for_todo(project, target, current_user, Todo::MARKED)
|
||||
create_todos(current_user, attributes, project&.namespace, project)
|
||||
end
|
||||
|
||||
def todo_exist?(issuable, current_user)
|
||||
|
|
@ -214,8 +215,9 @@ class TodoService
|
|||
end
|
||||
|
||||
def create_request_review_todo(target, author, reviewers)
|
||||
attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED)
|
||||
create_todos(reviewers, attributes)
|
||||
project = target.project
|
||||
attributes = attributes_for_todo(project, target, author, Todo::REVIEW_REQUESTED)
|
||||
create_todos(reviewers, attributes, project.namespace, project)
|
||||
end
|
||||
|
||||
def create_member_access_request(member)
|
||||
|
|
@ -225,12 +227,20 @@ class TodoService
|
|||
approvers = source.access_request_approvers_to_be_notified.map(&:user)
|
||||
return true if approvers.empty?
|
||||
|
||||
create_todos(approvers, attributes)
|
||||
if source.instance_of? Project
|
||||
project = source
|
||||
namespace = project.namespace
|
||||
else
|
||||
project = nil
|
||||
namespace = source
|
||||
end
|
||||
|
||||
create_todos(approvers, attributes, namespace, project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_todos(users, attributes)
|
||||
def create_todos(users, attributes, namespace, project)
|
||||
users = Array(users)
|
||||
|
||||
return if users.empty?
|
||||
|
|
@ -256,7 +266,7 @@ class TodoService
|
|||
|
||||
todos = users.map do |user|
|
||||
issue_type = attributes.delete(:issue_type)
|
||||
track_todo_creation(user, issue_type)
|
||||
track_todo_creation(user, issue_type, namespace, project)
|
||||
|
||||
Todo.create(attributes.merge(user_id: user.id))
|
||||
end
|
||||
|
|
@ -296,9 +306,10 @@ class TodoService
|
|||
|
||||
def create_assignment_todo(target, author, old_assignees = [])
|
||||
if target.assignees.any?
|
||||
project = target.project
|
||||
assignees = target.assignees - old_assignees
|
||||
attributes = attributes_for_todo(target.project, target, author, Todo::ASSIGNED)
|
||||
create_todos(assignees, attributes)
|
||||
attributes = attributes_for_todo(project, target, author, Todo::ASSIGNED)
|
||||
create_todos(assignees, attributes, project.namespace, project)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -313,22 +324,24 @@ class TodoService
|
|||
# Create Todos for directly addressed users
|
||||
directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
|
||||
create_todos(directly_addressed_users, attributes)
|
||||
create_todos(directly_addressed_users, attributes, parent&.namespace, parent)
|
||||
|
||||
# Create Todos for mentioned users
|
||||
mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users + directly_addressed_users)
|
||||
attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
|
||||
create_todos(mentioned_users, attributes)
|
||||
create_todos(mentioned_users, attributes, parent&.namespace, parent)
|
||||
end
|
||||
|
||||
def create_build_failed_todo(merge_request, todo_author)
|
||||
attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
|
||||
create_todos(todo_author, attributes)
|
||||
project = merge_request.project
|
||||
attributes = attributes_for_todo(project, merge_request, todo_author, Todo::BUILD_FAILED)
|
||||
create_todos(todo_author, attributes, project.namespace, project)
|
||||
end
|
||||
|
||||
def create_unmergeable_todo(merge_request, todo_author)
|
||||
attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::UNMERGEABLE)
|
||||
create_todos(todo_author, attributes)
|
||||
project = merge_request.project
|
||||
attributes = attributes_for_todo(project, merge_request, todo_author, Todo::UNMERGEABLE)
|
||||
create_todos(todo_author, attributes, project.namespace, project)
|
||||
end
|
||||
|
||||
def attributes_for_target(target)
|
||||
|
|
@ -392,10 +405,23 @@ class TodoService
|
|||
PendingTodosFinder.new(users, criteria).execute
|
||||
end
|
||||
|
||||
def track_todo_creation(user, issue_type)
|
||||
def track_todo_creation(user, issue_type, namespace, project)
|
||||
return unless issue_type == 'incident'
|
||||
|
||||
track_usage_event(:incident_management_incident_todo, user.id)
|
||||
event = "incident_management_incident_todo"
|
||||
track_usage_event(event, user.id)
|
||||
|
||||
return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace)
|
||||
|
||||
Gitlab::Tracking.event(
|
||||
self.class.to_s,
|
||||
event,
|
||||
project: project,
|
||||
namespace: namespace,
|
||||
user: user,
|
||||
label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
|
||||
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
|
||||
)
|
||||
end
|
||||
|
||||
def attributes_for_access_request_todos(source, author, action, note = nil)
|
||||
|
|
|
|||
|
|
@ -1659,6 +1659,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: package_repositories:packages_debian_process_package_file
|
||||
:worker_name: Packages::Debian::ProcessPackageFileWorker
|
||||
:feature_category: :package_registry
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: package_repositories:packages_go_sync_packages
|
||||
:worker_name: Packages::Go::SyncPackagesWorker
|
||||
:feature_category: :package_registry
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Debian
|
||||
class ProcessPackageFileWorker
|
||||
include ApplicationWorker
|
||||
include ::Packages::FIPS
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
data_consistency :always
|
||||
|
||||
deduplicate :until_executed
|
||||
idempotent!
|
||||
|
||||
queue_namespace :package_repositories
|
||||
feature_category :package_registry
|
||||
|
||||
def perform(package_file_id, user_id, distribution_name, component_name)
|
||||
raise DisabledError, 'Debian registry is not FIPS compliant' if Gitlab::FIPS.enabled?
|
||||
|
||||
@package_file_id = package_file_id
|
||||
@user_id = user_id
|
||||
@distribution_name = distribution_name
|
||||
@component_name = component_name
|
||||
|
||||
return unless package_file && user && distribution_name && component_name
|
||||
# return if file has already been processed
|
||||
return unless package_file.debian_file_metadatum&.unknown?
|
||||
|
||||
::Packages::Debian::ProcessPackageFileService.new(package_file, user, distribution_name, component_name).execute
|
||||
rescue StandardError => e
|
||||
raise if e.instance_of?(DisabledError)
|
||||
|
||||
Gitlab::ErrorTracking.log_exception(e, package_file_id: @package_file_id, user_id: @user_id,
|
||||
distribution_name: @distribution_name, component_name: @component_name)
|
||||
package_file.destroy!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def package_file
|
||||
::Packages::PackageFile.find_by_id(@package_file_id)
|
||||
end
|
||||
strong_memoize_attr :package_file
|
||||
|
||||
def user
|
||||
::User.find_by_id(@user_id)
|
||||
end
|
||||
strong_memoize_attr :user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
description: Mirrored Service Ping Redis metric. Count of unique users that published incidents per month
|
||||
category: StatusPage::PublishService
|
||||
action: incident_management_incident_published
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ee
|
||||
tiers:
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
description: Migrated Service Ping metric. Count of unique users adding alerts to the TODO list
|
||||
category: Mutations::AlertManagement::Alerts::Todo::Create
|
||||
action: incident_management_alert_todo
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223/diffs
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
description: Count of unique users assigning an alert per week. Migrated form Service Ping metric
|
||||
category: Mutations::AlertManagement::Alerts::SetAssignees
|
||||
action: incident_management_alert_assigned
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
description: Migrated from Service Ping metric. Count of unique users creating incidents
|
||||
category: Mutations::AlertManagement::CreateAlertIssue
|
||||
action: incident_management_incident_created
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
description: Count of unique users changing alert's status. Migrated from Service Ping metric
|
||||
category: Mutations::AlertManagement::UpdateAlertStatus
|
||||
action: incident_management_alert_status_changed
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
description: Count of unique users created timeline events
|
||||
category: IncidentManagement::TimelineEvents::CreateService
|
||||
action: incident_management_timeline_event_created
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: respond
|
||||
product_category: incident_management
|
||||
value_type: number
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: IncidentManagement::TimelineEvents::DestroyService
|
||||
action: incident_management_timeline_event_deleted
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Event migrates from Service Ping metric. Count of unique users deleted timeline events"
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: respond
|
||||
product_category: incident_management
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: IncidentManagement::TimelineEvents::UpdateService
|
||||
action: incident_management_timeline_event_edited
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Event migrated form Service Ping metric. Count of unique users edited timeline events"
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: respond
|
||||
product_category: incident_management
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: IssueLinks::CreateService
|
||||
action: incident_management_incident_relate
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of unique users adding issues per that are related to an incident. Migrated from Service Ping"
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: IssueLinks::DestroyService
|
||||
action: incident_management_incident_unrelate
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
milestone: "15.7"
|
||||
description: "Count of unique users removing issue that are related to an incident. Migrated from Service Ping metric"
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: Issues::CloseService
|
||||
action: incident_management_incident_closed
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of users closing incidents. Migrated from Service Ping metric."
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: Issues::ReopenService
|
||||
action: incident_management_incident_reopened
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of unique users reopening incidents. Migrated from Service Ping metric."
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: Issues::UpdateService
|
||||
action: incident_management_incident_change_confidential
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of unique users changing incidents to confidential. Event migrated from Service Ping metric."
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: Issues::ZoomLinkService
|
||||
action: incident_management_incident_zoom_meeting
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of unique users creating Zoom meetings about incidents. Event migrated from Service Ping metric."
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: Notes::CreateService
|
||||
action: incident_management_incident_comment
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of unique users adding comments on incidents. Event migrated from Service Ping metric"
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
category: TodoService
|
||||
action: incident_management_incident_todo
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
description: "Count of unique users adding incidents to the TODO list. Event migrated from Service Ping metric"
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
description: Count of unique users assiging incidents per
|
||||
category: Issues::UpdateService
|
||||
action: incident_management_incident_assigned
|
||||
label_description: "Mirrored Service Ping total metric key_path: redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
- project
|
||||
- user
|
||||
- namespace
|
||||
product_section: ops
|
||||
product_stage: monitor
|
||||
product_group: monitor
|
||||
product_category:
|
||||
milestone: "15.7"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105223
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -6,9 +6,11 @@ product_stage: create
|
|||
product_group: code_review
|
||||
product_category: code_review
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone: "15.6"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103334
|
||||
removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106449
|
||||
milestone_removed: "15.7"
|
||||
time_frame: 28d
|
||||
data_source: database
|
||||
data_category: optional
|
||||
|
|
|
|||
|
|
@ -18772,6 +18772,7 @@ Represents a requirement.
|
|||
| <a id="requirementtitlehtml"></a>`titleHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `title`. |
|
||||
| <a id="requirementupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the requirement was last updated. |
|
||||
| <a id="requirementuserpermissions"></a>`userPermissions` | [`RequirementPermissions!`](#requirementpermissions) | Permissions for the current user on the resource. |
|
||||
| <a id="requirementworkitemiid"></a>`workItemIid` | [`ID!`](#id) | Work item IID of the requirement, will replace current IID as identifier soon. |
|
||||
|
||||
#### Fields with arguments
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,6 @@ Contributions are welcome.
|
|||
For an introduction and basic steps, see
|
||||
[How to make GitLab API calls](https://www.youtube.com/watch?v=0LsMC3ZiXkA).
|
||||
|
||||
## SCIM API **(PREMIUM SAAS)**
|
||||
|
||||
GitLab provides a [SCIM API](scim.md) that both implements
|
||||
[the RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644) and provides the
|
||||
`/Users` endpoint. The base URL is `/api/scim/v2/groups/:group_path/Users/`.
|
||||
|
||||
## GraphQL API
|
||||
|
||||
A GraphQL API is available in GitLab.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98354) in GitLab 15.5.
|
||||
|
||||
GitLab provides an SCIM API that both implements [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644)
|
||||
and provides the `/Users` endpoint. The base URL is `/api/scim/v2/groups/:group_path/Users/`.
|
||||
|
||||
To use this API, [Group SSO](../user/group/saml_sso/index.md) must be enabled for the group.
|
||||
This API is only in use where [SCIM for Group SSO](../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities.
|
||||
|
||||
|
|
|
|||
|
|
@ -176,3 +176,56 @@ Parameters:
|
|||
| ---------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `tag_name` | string | yes | The name of a tag |
|
||||
|
||||
## Get X.509 signature of a tag
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106578) in GitLab 15.7.
|
||||
|
||||
Get the [X.509 signature from a tag](../user/project/repository/x509_signed_commits/index.md#sign-commits-and-tags-with-x509-certificates),
|
||||
if it is signed. Unsigned tags return a `404 Not Found` response.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/tags/:tag_name/signature
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `tag_name` | string | yes | The name of a tag. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/repository/tags/v1.1.1/signature"
|
||||
```
|
||||
|
||||
Example response if tag is X.509 signed:
|
||||
|
||||
```json
|
||||
{
|
||||
"signature_type": "X509",
|
||||
"verification_status": "unverified",
|
||||
"x509_certificate": {
|
||||
"id": 1,
|
||||
"subject": "CN=gitlab@example.org,OU=Example,O=World",
|
||||
"subject_key_identifier": "BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC",
|
||||
"email": "gitlab@example.org",
|
||||
"serial_number": 278969561018901340486471282831158785578,
|
||||
"certificate_status": "good",
|
||||
"x509_issuer": {
|
||||
"id": 1,
|
||||
"subject": "CN=PKI,OU=Example,O=World",
|
||||
"subject_key_identifier": "AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB",
|
||||
"crl_url": "http://example.com/pki.crl"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example response if tag is unsigned:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "404 GPG Signature Not Found"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
status: proposed
|
||||
creation-date: 2022-11-09
|
||||
creation-date: "2022-11-09"
|
||||
authors: [ "@ankitbhatnagar" ]
|
||||
coach: "@mappelman"
|
||||
approvers: [ "@sebastienpahl", "@nicholasklick" ]
|
||||
|
|
|
|||
|
|
@ -228,7 +228,10 @@ projects in a group, allowing tighter control over project membership.
|
|||
For example, if you want to lock the group for an [Audit Event](../../administration/audit_events.md),
|
||||
you can guarantee that project membership cannot be modified during the audit.
|
||||
|
||||
You can still invite groups or to add members to groups, implicitly giving members access to projects in the **locked** group.
|
||||
If group membership lock is enabled, the group owner can still:
|
||||
|
||||
- Invite groups or add members to groups to give them access to projects in the **locked** group.
|
||||
- Change the role of group members.
|
||||
|
||||
The setting does not cascade. Projects in subgroups observe the subgroup configuration, ignoring the parent group.
|
||||
|
||||
|
|
@ -239,8 +242,10 @@ To prevent members from being added to projects in a group:
|
|||
1. Under **Membership**, select **Users cannot be added to projects in this group**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
All users who previously had permissions can no longer add members to a group.
|
||||
API requests to add a new user to a project are not possible.
|
||||
After you lock the membership for a group:
|
||||
|
||||
- All users who previously had permissions can no longer add members to a group.
|
||||
- API requests to add a new user to a project are not possible.
|
||||
|
||||
## Manage group memberships via LDAP **(PREMIUM SELF)**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Source Code
|
||||
stage: Plan
|
||||
group: Project Management
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
|
|
@ -53,12 +53,12 @@ Global search only flags with an error any search that includes more than:
|
|||
To start a search, type your search query in the search bar on the top-right of the screen.
|
||||
You must type at least two characters.
|
||||
|
||||

|
||||

|
||||
|
||||
After the results are displayed, you can modify the search, select a different type of data to
|
||||
search, or choose a specific group or project.
|
||||
|
||||

|
||||

|
||||
|
||||
## Search in code
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class TagSignature < Grape::Entity
|
||||
expose :signature_type, documentation: { type: 'string', example: 'PGP' }
|
||||
|
||||
expose :signature, merge: true do |tag|
|
||||
::API::Entities::X509Signature.represent tag.signature if tag.signature_type == :X509
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -129,6 +129,24 @@ module API
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Get a tag's signature" do
|
||||
success code: 200, model: Entities::TagSignature
|
||||
tags %w[tags]
|
||||
failure [
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
end
|
||||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag'
|
||||
end
|
||||
get ':id/repository/tags/:tag_name/signature', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :source_code_management do
|
||||
tag = user_project.repository.find_tag(params[:tag_name])
|
||||
not_found! 'Tag' unless tag
|
||||
not_found! 'Signature' unless tag.has_signature?
|
||||
|
||||
present tag, with: Entities::TagSignature
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Metrics
|
||||
module Instrumentations
|
||||
class CountMergeRequestAuthorsMetric < DatabaseMetric
|
||||
operation :distinct_count, column: :author_id
|
||||
|
||||
relation { MergeRequest }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -43034,6 +43034,11 @@ msgstr ""
|
|||
msgid "To import an SVN repository, check out %{svn_link}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To invite more users, you can reduce the number of users in your namespace to %{free_limit} user or less. You can also upgrade to a paid tier which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users."
|
||||
msgid_plural "To invite more users, you can reduce the number of users in your namespace to %{free_limit} users or less. You can also upgrade to a paid tier which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "To keep this project going, create a new issue"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47997,6 +48002,9 @@ msgstr ""
|
|||
msgid "Your name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your namespace %{namespace_name} has reached the %{free_limit} user limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your namespace %{namespace_name} is over the %{free_limit} user limit and has been placed in a read-only state."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ function display_deployment_debug() {
|
|||
local namespace="${CI_ENVIRONMENT_SLUG}"
|
||||
|
||||
# Install dig to inspect DNS entries
|
||||
apt update && apt install dnsutils
|
||||
apt update && apt install -y dnsutils
|
||||
|
||||
echoinfo "[debugging data] Check review-app webservice DNS entry:"
|
||||
dig +short $(echo "${CI_ENVIRONMENT_URL}" | sed 's~http[s]*://~~g')
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ FactoryBot.define do
|
|||
|
||||
# Expect base_types to exist on the DB
|
||||
if type_base_attributes.slice(:namespace, :namespace_id).compact.empty?
|
||||
WorkItems::Type.find_or_initialize_by(type_base_attributes).tap { |type| type.assign_attributes(attributes) }
|
||||
WorkItems::Type.find_or_initialize_by(type_base_attributes)
|
||||
else
|
||||
WorkItems::Type.new(attributes)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import VariablesYaml from './yaml_tests/positive_tests/variables.yml';
|
|||
import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml';
|
||||
import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml';
|
||||
import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
|
||||
import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
|
||||
|
||||
// YAML NEGATIVE TEST
|
||||
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
|
||||
|
|
@ -50,6 +51,7 @@ import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variable
|
|||
import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml';
|
||||
import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml';
|
||||
import HooksNegative from './yaml_tests/negative_tests/hooks.yml';
|
||||
import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml';
|
||||
|
||||
const ajv = new Ajv({
|
||||
strictTypes: false,
|
||||
|
|
@ -87,6 +89,7 @@ describe('positive tests', () => {
|
|||
VariablesYaml,
|
||||
ProjectPathYaml,
|
||||
IdTokensYaml,
|
||||
SecretsYaml,
|
||||
}),
|
||||
)('schema validates %s', (_, input) => {
|
||||
// We construct a new "JSON" from each main key that is inside a
|
||||
|
|
@ -122,6 +125,7 @@ describe('negative tests', () => {
|
|||
ProjectPathIncludeLeadSlashYaml,
|
||||
ProjectPathIncludeNoSlashYaml,
|
||||
ProjectPathIncludeTailSlashYaml,
|
||||
SecretsNegativeYaml,
|
||||
TriggerNegative,
|
||||
HooksNegative,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
job_with_secrets_without_vault:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
token: $TEST_TOKEN
|
||||
|
||||
job_with_secrets_with_extra_properties:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault: test/db/password
|
||||
extra_prop: TEST
|
||||
|
||||
job_with_secrets_with_invalid_vault_property:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault:
|
||||
invalid: TEST
|
||||
|
||||
job_with_secrets_with_missing_required_vault_property:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault:
|
||||
path: gitlab
|
||||
|
||||
job_with_secrets_with_missing_required_engine_property:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault:
|
||||
engine:
|
||||
path: kv
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
valid_job_with_secrets:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault: test/db/password
|
||||
|
||||
valid_job_with_secrets_and_token:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault: test/db/password
|
||||
token: $TEST_TOKEN
|
||||
|
||||
valid_job_with_secrets_with_every_vault_keyword:
|
||||
script:
|
||||
- echo $TEST_DB_PASSWORD
|
||||
secrets:
|
||||
TEST_DB_PASSWORD:
|
||||
vault:
|
||||
engine:
|
||||
name: test-engine
|
||||
path: test
|
||||
path: test/db
|
||||
field: password
|
||||
file: true
|
||||
token: $TEST_TOKEN
|
||||
|
|
@ -56,6 +56,15 @@ RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees do
|
|||
context 'when operation mode is not specified' do
|
||||
it_behaves_like 'successful resolution'
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_alert_assigned
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_alert_assigned' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have permission to update alerts' do
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do
|
|||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_alert_todo
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_alert_todo' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
|
||||
context 'when user does not have permissions' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
|
|||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_incident_created
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_alert_create_incident
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_incident_created' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CreateAlertIssue responds with an error' do
|
||||
|
|
@ -46,6 +55,15 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
|
|||
errors: ['An issue already exists']
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_incident_created' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
|
|||
let(:user) { current_user }
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_alert_status_changed' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
|
||||
context 'error occurs when updating' do
|
||||
it 'returns the alert with errors' do
|
||||
# Stub an error on the alert
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@ RSpec.describe Groups::ObservabilityHelper do
|
|||
context 'if observability_path is missing from params' do
|
||||
it 'returns the iframe src for action: dashboards' do
|
||||
allow(helper).to receive(:params).and_return({ action: 'dashboards' })
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/")
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/")
|
||||
end
|
||||
|
||||
it 'returns the iframe src for action: manage' do
|
||||
allow(helper).to receive(:params).and_return({ action: 'manage' })
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/dashboards")
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/dashboards")
|
||||
end
|
||||
|
||||
it 'returns the iframe src for action: explore' do
|
||||
allow(helper).to receive(:params).and_return({ action: 'explore' })
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/explore")
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/explore")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ RSpec.describe Groups::ObservabilityHelper do
|
|||
context 'if observability_path is valid' do
|
||||
it 'returns the iframe src by injecting the observability path' do
|
||||
allow(helper).to receive(:params).and_return({ action: '/explore', observability_path: '/foo?bar=foobar' })
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/foo?bar=foobar")
|
||||
expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/foo?bar=foobar")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ RSpec.describe Groups::ObservabilityHelper do
|
|||
"/test?groupId=<script>alert('attack!')</script>"
|
||||
})
|
||||
expect(helper.observability_iframe_src(group)).to eq(
|
||||
"#{observability_url}/#{group.id}/test?groupId=alert('attack!')"
|
||||
"#{observability_url}/-/#{group.id}/test?groupId=alert('attack!')"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -162,6 +162,13 @@ RSpec.describe Feature, stub_feature_flags: false do
|
|||
stub_feature_flag_definition(:enabled_feature_flag, default_enabled: true)
|
||||
end
|
||||
|
||||
context 'when using redis cache', :use_clean_rails_redis_caching do
|
||||
it 'does not make recursive feature-flag calls' do
|
||||
expect(described_class).to receive(:enabled?).once.and_call_original
|
||||
described_class.enabled?(:disabled_feature_flag)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when self-recursive' do
|
||||
before do
|
||||
allow(Feature).to receive(:with_feature).and_wrap_original do |original, name, &block|
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountMergeRequestAuthorsMetric do
|
||||
let(:expected_value) { 1 }
|
||||
let(:start) { 30.days.ago.to_s(:db) }
|
||||
let(:finish) { 2.days.ago.to_s(:db) }
|
||||
|
||||
let(:expected_query) do
|
||||
"SELECT COUNT(DISTINCT \"merge_requests\".\"author_id\") FROM \"merge_requests\"" \
|
||||
" WHERE \"merge_requests\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"
|
||||
end
|
||||
|
||||
before do
|
||||
user = create(:user)
|
||||
user2 = create(:user)
|
||||
|
||||
create(:merge_request, created_at: 1.year.ago, author: user)
|
||||
create(:merge_request, created_at: 1.week.ago, author: user2)
|
||||
create(:merge_request, created_at: 1.week.ago, author: user2)
|
||||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' }
|
||||
end
|
||||
|
|
@ -632,7 +632,7 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
|
|||
end
|
||||
|
||||
context 'when unsupported widget input is sent' do
|
||||
let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
|
||||
let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
|
||||
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
|
||||
|
||||
let(:input) do
|
||||
|
|
@ -642,7 +642,7 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
|
|||
end
|
||||
|
||||
it_behaves_like 'a mutation that returns top-level errors',
|
||||
errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
|
||||
errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -479,4 +479,60 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/repository/tags/:tag_name/signature' do
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
let(:project_id) { project.id }
|
||||
let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}/signature" }
|
||||
|
||||
context 'when tag does not exist' do
|
||||
let(:tag_name) { 'unknown' }
|
||||
|
||||
it_behaves_like '404 response' do
|
||||
let(:request) { get api(route, current_user) }
|
||||
let(:message) { '404 Tag Not Found' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'unsigned tag' do
|
||||
let(:tag_name) { 'v1.1.0' }
|
||||
|
||||
it_behaves_like '404 response' do
|
||||
let(:request) { get api(route, current_user) }
|
||||
let(:message) { '404 Signature Not Found' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'x509 signed tag' do
|
||||
let(:tag_name) { 'v1.1.1' }
|
||||
let(:tag) { project.repository.find_tag(tag_name) }
|
||||
let(:signature) { tag.signature }
|
||||
let(:x509_certificate) { signature.x509_certificate }
|
||||
let(:x509_issuer) { x509_certificate.x509_issuer }
|
||||
|
||||
it 'returns correct JSON' do
|
||||
get api(route, current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to eq(
|
||||
'signature_type' => 'X509',
|
||||
'verification_status' => signature.verification_status.to_s,
|
||||
'x509_certificate' => {
|
||||
'id' => x509_certificate.id,
|
||||
'subject' => x509_certificate.subject,
|
||||
'subject_key_identifier' => x509_certificate.subject_key_identifier,
|
||||
'email' => x509_certificate.email,
|
||||
'serial_number' => x509_certificate.serial_number,
|
||||
'certificate_status' => x509_certificate.certificate_status,
|
||||
'x509_issuer' => {
|
||||
'id' => x509_issuer.id,
|
||||
'subject' => x509_issuer.subject,
|
||||
'subject_key_identifier' => x509_issuer.subject_key_identifier,
|
||||
'crl_url' => x509_issuer.crl_url
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -73,21 +73,21 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
|
|||
|
||||
describe 'GET #dashboards' do
|
||||
let(:path) { group_observability_dashboards_path(group) }
|
||||
let(:expected_observability_path) { "#{observability_url}/#{group.id}/" }
|
||||
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/" }
|
||||
|
||||
it_behaves_like 'observability route request'
|
||||
end
|
||||
|
||||
describe 'GET #manage' do
|
||||
let(:path) { group_observability_manage_path(group) }
|
||||
let(:expected_observability_path) { "#{observability_url}/#{group.id}/dashboards" }
|
||||
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/dashboards" }
|
||||
|
||||
it_behaves_like 'observability route request'
|
||||
end
|
||||
|
||||
describe 'GET #explore' do
|
||||
let(:path) { group_observability_explore_path(group) }
|
||||
let(:expected_observability_path) { "#{observability_url}/#{group.id}/explore" }
|
||||
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/explore" }
|
||||
|
||||
it_behaves_like 'observability route request'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -55,6 +55,15 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
|
|||
end
|
||||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_timeline_event_created' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
|
||||
subject(:execute) { service.execute }
|
||||
|
|
@ -276,6 +285,15 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
|
|||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_timeline_event_created' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
|
||||
it 'successfully creates a database record', :aggregate_failures do
|
||||
expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ RSpec.describe IncidentManagement::TimelineEvents::DestroyService do
|
|||
end
|
||||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_deleted
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:user) { current_user }
|
||||
let(:action) { 'incident_management_timeline_event_deleted' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
|
|||
end
|
||||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_edited
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace.reload }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_timeline_event_edited' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'error response' do |message|
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ RSpec.describe IssueLinks::CreateService do
|
|||
it_behaves_like 'an incident management tracked event', :incident_management_incident_relate do
|
||||
let(:current_user) { user }
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_relate' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ RSpec.describe IssueLinks::DestroyService do
|
|||
it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do
|
||||
let(:current_user) { user }
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue_b.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_unrelate' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -99,6 +99,14 @@ RSpec.describe Issues::CloseService do
|
|||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_incident_closed
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_closed' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
|
||||
it 'creates a new escalation resolved escalation status', :aggregate_failures do
|
||||
expect { service.execute(issue) }.to change { IncidentManagement::IssuableEscalationStatus.where(issue: issue).count }.by(1)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,21 +9,22 @@ RSpec.describe Issues::CreateService do
|
|||
let_it_be_with_reload(:project) { create(:project, :public, group: group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:opts) { { title: 'title' } }
|
||||
let(:spam_params) { double }
|
||||
let(:service) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params) }
|
||||
|
||||
it_behaves_like 'rate limited service' do
|
||||
let(:key) { :issues_create }
|
||||
let(:key_scope) { %i[project current_user external_author] }
|
||||
let(:application_limit_key) { :issues_create_limit }
|
||||
let(:created_model) { Issue }
|
||||
let(:service) { described_class.new(project: project, current_user: user, params: { title: 'title' }, spam_params: double) }
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let_it_be(:assignee) { create(:user) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||
|
||||
let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
|
||||
let(:result) { service.execute }
|
||||
let(:issue) { result[:issue] }
|
||||
|
||||
before do
|
||||
|
|
@ -54,6 +55,7 @@ RSpec.describe Issues::CreateService do
|
|||
|
||||
let(:opts) do
|
||||
{ title: 'Awesome issue',
|
||||
issue_type: :task,
|
||||
description: 'please fix',
|
||||
assignee_ids: [assignee.id],
|
||||
label_ids: labels.map(&:id),
|
||||
|
|
@ -118,10 +120,26 @@ RSpec.describe Issues::CreateService do
|
|||
expect(issue.labels).to match_array(labels)
|
||||
expect(issue.milestone).to eq(milestone)
|
||||
expect(issue.due_date).to eq(Date.tomorrow)
|
||||
expect(issue.work_item_type.base_type).to eq('issue')
|
||||
expect(issue.work_item_type.base_type).to eq('task')
|
||||
expect(issue.issue_customer_relations_contacts).to be_empty
|
||||
end
|
||||
|
||||
context 'when the work item type is not allowed to create' do
|
||||
before do
|
||||
allow_next_instance_of(::Issues::BuildService) do |instance|
|
||||
allow(instance).to receive(:create_issue_type_allowed?).twice.and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'ignores the type and creates default issue' do
|
||||
expect(result).to be_success
|
||||
expect(issue).to be_persisted
|
||||
expect(issue).to be_a(::Issue)
|
||||
expect(issue.work_item_type.base_type).to eq('issue')
|
||||
expect(issue.issue_type).to eq('issue')
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls NewIssueWorker with correct arguments' do
|
||||
expect(NewIssueWorker).to receive(:perform_async).with(Integer, user.id, 'Issue')
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,14 @@ RSpec.describe Issues::ReopenService do
|
|||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_incident_reopened
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_reopened' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
|
||||
it 'creates a timeline event' do
|
||||
expect(IncidentManagement::TimelineEvents::CreateService)
|
||||
.to receive(:reopen_incident)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
description: 'Also please fix',
|
||||
assignee_ids: [user2.id],
|
||||
state_event: 'close',
|
||||
label_ids: [label.id],
|
||||
label_ids: [label&.id],
|
||||
due_date: Date.tomorrow,
|
||||
discussion_locked: true,
|
||||
severity: 'low',
|
||||
|
|
@ -189,6 +189,27 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
subject { update_issue(confidential: true) }
|
||||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_incident_change_confidential
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
let(:action) { 'incident_management_incident_change_confidential' }
|
||||
let(:opts) do
|
||||
{
|
||||
title: 'New title',
|
||||
description: 'Also please fix',
|
||||
assignee_ids: [user2.id],
|
||||
state_event: 'close',
|
||||
due_date: Date.tomorrow,
|
||||
discussion_locked: true,
|
||||
severity: 'low',
|
||||
milestone_id: milestone.id,
|
||||
add_contacts: [contact.email]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -673,6 +694,14 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
let(:current_user) { user }
|
||||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_incident_assigned
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
let(:action) { "incident_management_incident_assigned" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,14 @@ RSpec.describe Issues::ZoomLinkService do
|
|||
let(:current_user) { user }
|
||||
|
||||
it_behaves_like 'an incident management tracked event', :incident_management_incident_zoom_meeting
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_zoom_meeting' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with insufficient issue update permissions' do
|
||||
|
|
|
|||
|
|
@ -102,6 +102,14 @@ RSpec.describe Notes::CreateService do
|
|||
it_behaves_like 'an incident management tracked event', :incident_management_incident_comment do
|
||||
let(:current_user) { user }
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { issue.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_comment' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'event tracking', :snowplow do
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
|
|||
end
|
||||
|
||||
context 'with already processed package file' do
|
||||
let!(:package_file) { create(:debian_package_file) }
|
||||
let_it_be(:package_file) { create(:debian_package_file) }
|
||||
|
||||
let(:component_name) { 'main' }
|
||||
|
||||
it 'raise ArgumentError', :aggregate_failures do
|
||||
|
|
|
|||
|
|
@ -209,6 +209,15 @@ RSpec.describe TodoService do
|
|||
it_behaves_like 'an incident management tracked event', :incident_management_incident_todo do
|
||||
let(:current_user) { john_doe }
|
||||
end
|
||||
|
||||
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
|
||||
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
|
||||
let(:namespace) { project.namespace }
|
||||
let(:category) { described_class.to_s }
|
||||
let(:action) { 'incident_management_incident_todo' }
|
||||
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
|
||||
let(:user) { john_doe }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1251,6 +1260,19 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
describe '#create_member_access_request' do
|
||||
context 'snowplow event tracking' do
|
||||
it 'does not track snowplow event when todos are for access request for project', :snowplow do
|
||||
user = create(:user)
|
||||
project = create(:project)
|
||||
requester = create(:project_member, project: project, user: assignee)
|
||||
project.add_owner(user)
|
||||
|
||||
expect_no_snowplow_event
|
||||
|
||||
service.create_member_access_request(requester)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the group has more than 10 owners' do
|
||||
it 'creates todos for 10 recently active group owners' do
|
||||
group = create(:group, :public)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Debian::ProcessPackageFileWorker, type: :worker, feature_category: :package_registry do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
|
||||
|
||||
let(:incoming) { create(:debian_incoming, project: distribution.project) }
|
||||
let(:distribution_name) { distribution.codename }
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
describe '#perform' do
|
||||
let(:package_file_id) { package_file.id }
|
||||
let(:user_id) { user.id }
|
||||
|
||||
subject { worker.perform(package_file_id, user_id, distribution_name, component_name) }
|
||||
|
||||
shared_examples 'returns early without error' do
|
||||
it 'returns early without error' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
|
||||
expect(::Packages::Debian::ProcessPackageFileService).not_to receive(:new)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:case_name, :expected_file_type, :file_name, :component_name) do
|
||||
'with a deb' | 'deb' | 'libsample0_1.2.3~alpha2_amd64.deb' | 'main'
|
||||
'with an udeb' | 'udeb' | 'sample-udeb_1.2.3~alpha2_amd64.udeb' | 'contrib'
|
||||
end
|
||||
|
||||
with_them do
|
||||
context 'with Debian package file' do
|
||||
let(:package_file) { incoming.package_files.with_file_name(file_name).first }
|
||||
|
||||
context 'with mocked service' do
|
||||
it 'calls ProcessPackageFileService' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
|
||||
expect_next_instance_of(::Packages::Debian::ProcessPackageFileService) do |service|
|
||||
expect(service).to receive(:execute)
|
||||
.with(no_args)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non existing user' do
|
||||
let(:user_id) { non_existing_record_id }
|
||||
|
||||
it_behaves_like 'returns early without error'
|
||||
end
|
||||
|
||||
context 'with nil user id' do
|
||||
let(:user_id) { nil }
|
||||
|
||||
it_behaves_like 'returns early without error'
|
||||
end
|
||||
|
||||
context 'when the service raises an error' do
|
||||
let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first }
|
||||
|
||||
it 'removes package file', :aggregate_failures do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
||||
instance_of(ArgumentError),
|
||||
package_file_id: package_file_id,
|
||||
user_id: user_id,
|
||||
distribution_name: distribution_name,
|
||||
component_name: component_name
|
||||
)
|
||||
expect { subject }
|
||||
.to not_change(Packages::Package, :count)
|
||||
.and change { Packages::PackageFile.count }.by(-1)
|
||||
.and change { incoming.package_files.count }.from(7).to(6)
|
||||
|
||||
expect { package_file.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'an idempotent worker' do
|
||||
let(:job_args) { [package_file.id, user.id, distribution_name, component_name] }
|
||||
|
||||
it 'sets the Debian file type as deb', :aggregate_failures do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
|
||||
|
||||
# Using subject inside this block will process the job multiple times
|
||||
expect { subject }
|
||||
.to change { Packages::Package.count }.from(1).to(2)
|
||||
.and not_change(Packages::PackageFile, :count)
|
||||
.and change { incoming.package_files.count }.from(7).to(6)
|
||||
.and change {
|
||||
package_file&.debian_file_metadatum&.reload&.file_type
|
||||
}.from('unknown').to(expected_file_type)
|
||||
|
||||
created_package = Packages::Package.last
|
||||
expect(created_package.name).to eq 'sample'
|
||||
expect(created_package.version).to eq '1.2.3~alpha2'
|
||||
expect(created_package.creator).to eq user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with already processed package file' do
|
||||
let_it_be(:package_file) { create(:debian_package_file) }
|
||||
|
||||
let(:component_name) { 'main' }
|
||||
|
||||
it_behaves_like 'returns early without error'
|
||||
end
|
||||
|
||||
context 'with a deb' do
|
||||
let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
|
||||
let(:component_name) { 'main' }
|
||||
|
||||
context 'with FIPS mode enabled', :fips_mode do
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(::Packages::FIPS::DisabledError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non existing package file' do
|
||||
let(:package_file_id) { non_existing_record_id }
|
||||
|
||||
it_behaves_like 'returns early without error'
|
||||
end
|
||||
|
||||
context 'with nil package file id' do
|
||||
let(:package_file_id) { nil }
|
||||
|
||||
it_behaves_like 'returns early without error'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -67,11 +67,8 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, fi
|
|||
// Create multipart reader
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
if err == http.ErrNotMultipart {
|
||||
// We want to be able to recognize http.ErrNotMultipart elsewhere so no fmt.Errorf
|
||||
return http.ErrNotMultipart
|
||||
}
|
||||
return fmt.Errorf("get multipart reader: %v", err)
|
||||
// We want to be able to recognize these errors elsewhere so no fmt.Errorf
|
||||
return err
|
||||
}
|
||||
|
||||
multipartUploadRequests.WithLabelValues(filter.Name()).Inc()
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func interceptMultipartFiles(w http.ResponseWriter, r *http.Request, h http.Hand
|
|||
err := rewriteFormFilesFromMultipart(r, writer, filter, fa, p)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case ErrInjectedClientParam:
|
||||
case ErrInjectedClientParam, http.ErrMissingBoundary:
|
||||
helper.CaptureAndFail(w, r, err, "Bad Request", http.StatusBadRequest)
|
||||
case ErrTooManyFilesUploaded:
|
||||
helper.CaptureAndFail(w, r, err, err.Error(), http.StatusBadRequest)
|
||||
|
|
|
|||
|
|
@ -352,6 +352,18 @@ func TestInvalidFileNames(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBadMultipartHeader(t *testing.T) {
|
||||
httpRequest, err := http.NewRequest("POST", "/example", bytes.NewReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Invalid header: missing boundary
|
||||
httpRequest.Header.Set("Content-Type", "multipart/form-data")
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
testInterceptMultipartFiles(t, response, httpRequest, nilHandler, &SavedFileTracker{Request: httpRequest})
|
||||
require.Equal(t, 400, response.Code)
|
||||
}
|
||||
|
||||
func TestContentDispositionRewrite(t *testing.T) {
|
||||
testhelper.ConfigureSecret()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue