summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-04 09:12:56 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-04 09:12:56 +0000
commit191020103bd4d2aad99c62a35201c05d9df74f8f (patch)
tree83ef6f71fea1f66842545e9af788fdc33d34d48b
parent5bfd7a344b73d6a9482b420fa7646f7d1760a566 (diff)
downloadgitlab-ce-191020103bd4d2aad99c62a35201c05d9df74f8f.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md5
-rw-r--r--app/controllers/explore/projects_controller.rb9
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb80
-rw-r--r--app/helpers/nav/top_nav_helper.rb1
-rw-r--r--app/helpers/workhorse_helper.rb4
-rw-r--r--app/models/analytics/cycle_analytics/issue_stage_event.rb6
-rw-r--r--app/models/analytics/cycle_analytics/merge_request_stage_event.rb6
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb13
-rw-r--r--app/models/dependency_proxy/blob.rb2
-rw-r--r--app/models/dependency_proxy/manifest.rb9
-rw-r--r--app/services/dependency_proxy/find_or_create_manifest_service.rb30
-rw-r--r--app/views/dashboard/_projects_head.html.haml14
-rw-r--r--app/views/dashboard/_projects_nav.html.haml18
-rw-r--r--app/views/explore/projects/topics.html.haml12
-rw-r--r--app/views/explore/topics/_head.html.haml9
-rw-r--r--app/views/jira_connect/branches/new.html.haml1
-rw-r--r--app/views/shared/topics/_list.html.haml9
-rw-r--r--app/views/shared/topics/_topic.html.haml16
-rw-r--r--config/feature_flags/development/dependency_proxy_manifest_workhorse.yml (renamed from config/feature_flags/development/new_customersdot_staging_url.yml)8
-rw-r--r--config/feature_flags/development/use_vsa_aggregated_tables.yml8
-rw-r--r--config/routes/explore.rb1
-rw-r--r--config/routes/group.rb2
-rw-r--r--doc/administration/instance_limits.md11
-rw-r--r--doc/administration/reference_architectures/10k_users.md7
-rw-r--r--doc/administration/reference_architectures/1k_users.md5
-rw-r--r--doc/administration/reference_architectures/25k_users.md7
-rw-r--r--doc/administration/reference_architectures/2k_users.md5
-rw-r--r--doc/administration/reference_architectures/3k_users.md7
-rw-r--r--doc/administration/reference_architectures/50k_users.md7
-rw-r--r--doc/administration/reference_architectures/5k_users.md7
-rw-r--r--doc/ci/index.md10
-rw-r--r--doc/ci/yaml/index.md49
-rw-r--r--doc/integration/jira/dvcs.md1
-rw-r--r--doc/user/project/integrations/webhooks.md10
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb125
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb48
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/median.rb36
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb41
-rw-r--r--lib/gitlab/analytics/cycle_analytics/data_collector.rb20
-rw-r--r--lib/gitlab/analytics/cycle_analytics/request_params.rb3
-rw-r--r--lib/gitlab/database/load_balancing/load_balancer.rb5
-rw-r--r--lib/gitlab/graphql/tracers/logger_tracer.rb8
-rw-r--r--lib/gitlab/subscription_portal.rb6
-rw-r--r--lib/gitlab/workhorse.rb5
-rw-r--r--locale/gitlab.pot3
-rw-r--r--rubocop/cop/qa/duplicate_testcase_link.rb52
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb126
-rw-r--r--spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb1
-rw-r--r--spec/factories/analytics/cycle_analytics/issue_stage_events.rb13
-rw-r--r--spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb13
-rw-r--r--spec/features/explore/topics_spec.rb25
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb150
-rw-r--r--spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb18
-rw-r--r--spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb2
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb31
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb18
-rw-r--r--spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb5
-rw-r--r--spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb5
-rw-r--r--spec/models/dependency_proxy/manifest_spec.rb10
-rw-r--r--spec/requests/api/graphql_spec.rb2
-rw-r--r--spec/requests/rack_attack_global_spec.rb4
-rw-r--r--spec/routing/group_routing_spec.rb20
-rw-r--r--spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb36
-rw-r--r--spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb60
-rw-r--r--spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb104
-rw-r--r--workhorse/internal/dependencyproxy/dependencyproxy.go13
-rw-r--r--workhorse/internal/dependencyproxy/dependencyproxy_test.go11
68 files changed, 1221 insertions, 192 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
index 76dd8077889..d4bd4f66720 100644
--- a/.gitlab/issue_templates/Feature Flag Roll Out.md
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -77,7 +77,10 @@ Are there any other stages or teams involved that need to be kept in the loop?
### Global rollout on production
-All `/chatops` commands that target production should be done in the `#production` slack channel for visibility.
+For visibility, all `/chatops` commands that target production should be:
+
+- Executed in the `#production` slack channel.
+- Cross-posted (with the command results) to the responsible team's slack channel (`#g_TEAM_NAME`).
- [ ] [Incrementally roll out](https://docs.gitlab.com/ee/development/feature_flags/controls.html#process) the feature.
- If the feature flag in code has [an actor](https://docs.gitlab.com/ee/development/feature_flags/#feature-actors), perform **actor-based** rollout.
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 81e1643a8f8..14dd2ae5691 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -68,6 +68,11 @@ class Explore::ProjectsController < Explore::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def topics
+ load_project_counts
+ load_topics
+ end
+
def topic
load_topic
@@ -95,6 +100,10 @@ class Explore::ProjectsController < Explore::ApplicationController
prepare_projects_for_rendering(projects)
end
+ def load_topics
+ @topics = Projects::TopicsFinder.new(params: params.permit(:search)).execute.page(params[:page]).without_count
+ end
+
def load_topic
@topic = Projects::Topic.find_by_name(params[:topic_name])
end
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index ef52800a203..5e6195296f7 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -11,8 +11,8 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
before_action :ensure_token_granted!, only: [:blob, :manifest]
before_action :ensure_feature_enabled!
- before_action :verify_workhorse_api!, only: [:authorize_upload_blob, :upload_blob]
- skip_before_action :verify_authenticity_token, only: [:authorize_upload_blob, :upload_blob]
+ before_action :verify_workhorse_api!, only: [:authorize_upload_blob, :upload_blob, :authorize_upload_manifest, :upload_manifest]
+ skip_before_action :verify_authenticity_token, only: [:authorize_upload_blob, :upload_blob, :authorize_upload_manifest, :upload_manifest]
attr_reader :token
@@ -22,20 +22,11 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
result = DependencyProxy::FindOrCreateManifestService.new(group, image, tag, token).execute
if result[:status] == :success
- response.headers['Docker-Content-Digest'] = result[:manifest].digest
- response.headers['Content-Length'] = result[:manifest].size
- response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
- response.headers['Etag'] = "\"#{result[:manifest].digest}\""
- content_type = result[:manifest].content_type
-
- event_name = tracking_event_name(object_type: :manifest, from_cache: result[:from_cache])
- track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
- send_upload(
- result[:manifest].file,
- proxy: true,
- redirect_params: { query: { 'response-content-type' => content_type } },
- send_params: { type: content_type }
- )
+ if result[:manifest]
+ send_manifest(result[:manifest], from_cache: result[:from_cache])
+ else
+ send_dependency(manifest_header, DependencyProxy::Registry.manifest_url(image, tag), manifest_file_name)
+ end
else
render status: result[:http_status], json: result[:message]
end
@@ -59,7 +50,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
def authorize_upload_blob
set_workhorse_internal_api_content_type
- render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false, maximum_size: 5.gigabytes)
+ render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false, maximum_size: DependencyProxy::Blob::MAX_FILE_SIZE)
end
def upload_blob
@@ -75,6 +66,27 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
head :ok
end
+ def authorize_upload_manifest
+ set_workhorse_internal_api_content_type
+
+ render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false, maximum_size: DependencyProxy::Manifest::MAX_FILE_SIZE)
+ end
+
+ def upload_manifest
+ @group.dependency_proxy_manifests.create!(
+ file_name: manifest_file_name,
+ content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER],
+ digest: request.headers['Docker-Content-Digest'],
+ file: params[:file],
+ size: params[:file].size
+ )
+
+ event_name = tracking_event_name(object_type: :manifest, from_cache: false)
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
+
+ head :ok
+ end
+
private
def blob_via_workhorse
@@ -86,14 +98,38 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
send_upload(blob.file)
else
- send_dependency(token, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
+ send_dependency(token_header, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
end
end
+ def send_manifest(manifest, from_cache:)
+ # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
+ manifest.touch
+ response.headers['Docker-Content-Digest'] = manifest.digest
+ response.headers['Content-Length'] = manifest.size
+ response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
+ response.headers['Etag'] = "\"#{manifest.digest}\""
+ content_type = manifest.content_type
+
+ event_name = tracking_event_name(object_type: :manifest, from_cache: from_cache)
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
+
+ send_upload(
+ manifest.file,
+ proxy: true,
+ redirect_params: { query: { 'response-content-type' => content_type } },
+ send_params: { type: content_type }
+ )
+ end
+
def blob_file_name
@blob_file_name ||= params[:sha].sub('sha256:', '') + '.gz'
end
+ def manifest_file_name
+ @manifest_file_name ||= "#{image}:#{tag}.json"
+ end
+
def group
strong_memoize(:group) do
Group.find_by_full_path(params[:group_id], follow_redirects: true)
@@ -137,4 +173,12 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
render status: result[:http_status], json: result[:message]
end
end
+
+ def token_header
+ { Authorization: ["Bearer #{token}"] }
+ end
+
+ def manifest_header
+ token_header.merge(Accept: ::ContainerRegistry::Client::ACCEPTED_TYPES)
+ end
end
diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb
index 3055ad57b80..ecef2d38e54 100644
--- a/app/helpers/nav/top_nav_helper.rb
+++ b/app/helpers/nav/top_nav_helper.rb
@@ -267,6 +267,7 @@ module Nav
builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path)
builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path)
+ builder.add_primary_menu_item(id: 'topics', title: _('Explore topics'), href: topics_explore_projects_path)
builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
builder.build
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 4862282bc73..2460c956bb6 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -41,8 +41,8 @@ module WorkhorseHelper
head :ok
end
- def send_dependency(token, url, filename)
- headers.store(*Gitlab::Workhorse.send_dependency(token, url))
+ def send_dependency(dependency_headers, url, filename)
+ headers.store(*Gitlab::Workhorse.send_dependency(dependency_headers, url))
headers['Content-Disposition'] =
ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
headers['Content-Type'] = 'application/gzip'
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
index 333bd20328c..099bb671f10 100644
--- a/app/models/analytics/cycle_analytics/issue_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -11,6 +11,12 @@ module Analytics
alias_attribute :state, :state_id
enum state: Issue.available_states, _suffix: true
+ scope :assigned_to, ->(user) do
+ assignees_class = IssueAssignee
+ condition = assignees_class.where(user_id: user).where(arel_table[:issue_id].eq(assignees_class.arel_table[:issue_id]))
+ where(condition.arel.exists)
+ end
+
def self.issuable_id_column
:issue_id
end
diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
index 537e45058aa..632b7079b2e 100644
--- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -11,6 +11,12 @@ module Analytics
alias_attribute :state, :state_id
enum state: MergeRequest.available_states, _suffix: true
+ scope :assigned_to, ->(user) do
+ assignees_class = MergeRequestAssignee
+ condition = assignees_class.where(user_id: user).where(arel_table[:merge_request_id].eq(assignees_class.arel_table[:merge_request_id]))
+ where(condition.arel.exists)
+ end
+
def self.issuable_id_column
:merge_request_id
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 99a205d1574..cca1a0b41cc 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -5,6 +5,19 @@ module Analytics
module StageEventModel
extend ActiveSupport::Concern
+ included do
+ scope :by_stage_event_hash_id, ->(id) { where(stage_event_hash_id: id) }
+ scope :by_project_id, ->(id) { where(project_id: id) }
+ scope :by_group_id, ->(id) { where(group_id: id) }
+ scope :end_event_timestamp_after, -> (date) { where(arel_table[:end_event_timestamp].gteq(date)) }
+ scope :end_event_timestamp_before, -> (date) { where(arel_table[:end_event_timestamp].lteq(date)) }
+ scope :start_event_timestamp_after, -> (date) { where(arel_table[:start_event_timestamp].gteq(date)) }
+ scope :start_event_timestamp_before, -> (date) { where(arel_table[:start_event_timestamp].lteq(date)) }
+ scope :authored, ->(user) { where(author_id: user) }
+ scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
+ scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
+ end
+
class_methods do
def upsert_data(data)
upsert_values = data.map do |row|
diff --git a/app/models/dependency_proxy/blob.rb b/app/models/dependency_proxy/blob.rb
index 7ca15652586..bd5c022e692 100644
--- a/app/models/dependency_proxy/blob.rb
+++ b/app/models/dependency_proxy/blob.rb
@@ -7,6 +7,8 @@ class DependencyProxy::Blob < ApplicationRecord
belongs_to :group
+ MAX_FILE_SIZE = 5.gigabytes.freeze
+
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb
index b83047efe54..6a5ccd12cac 100644
--- a/app/models/dependency_proxy/manifest.rb
+++ b/app/models/dependency_proxy/manifest.rb
@@ -7,6 +7,8 @@ class DependencyProxy::Manifest < ApplicationRecord
belongs_to :group
+ MAX_FILE_SIZE = 10.megabytes.freeze
+
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
@@ -14,10 +16,7 @@ class DependencyProxy::Manifest < ApplicationRecord
mount_file_store_uploader DependencyProxy::FileUploader
- def self.find_or_initialize_by_file_name_or_digest(file_name:, digest:)
- result = find_by(file_name: file_name) || find_by(digest: digest)
- return result if result
-
- new(file_name: file_name, digest: digest)
+ def self.find_by_file_name_or_digest(file_name:, digest:)
+ find_by(file_name: file_name) || find_by(digest: digest)
end
end
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index 1976d4d47f4..055980c431f 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -14,18 +14,18 @@ module DependencyProxy
def execute
@manifest = @group.dependency_proxy_manifests
.active
- .find_or_initialize_by_file_name_or_digest(file_name: @file_name, digest: @tag)
+ .find_by_file_name_or_digest(file_name: @file_name, digest: @tag)
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
- if cached_manifest_matches?(head_result)
- @manifest.touch
+ return respond if cached_manifest_matches?(head_result)
- return success(manifest: @manifest, from_cache: true)
+ if Feature.enabled?(:dependency_proxy_manifest_workhorse, @group, default_enabled: :yaml)
+ success(manifest: nil, from_cache: false)
+ else
+ pull_new_manifest
+ respond(from_cache: false)
end
-
- pull_new_manifest
- respond(from_cache: false)
rescue Timeout::Error, *Gitlab::HTTP::HTTP_ERRORS
respond
end
@@ -34,12 +34,19 @@ module DependencyProxy
def pull_new_manifest
DependencyProxy::PullManifestService.new(@image, @tag, @token).execute_with_manifest do |new_manifest|
- @manifest.update!(
+ params = {
+ file_name: @file_name,
content_type: new_manifest[:content_type],
digest: new_manifest[:digest],
file: new_manifest[:file],
size: new_manifest[:file].size
- )
+ }
+
+ if @manifest
+ @manifest.update!(params)
+ else
+ @manifest = @group.dependency_proxy_manifests.create!(params)
+ end
end
end
@@ -50,10 +57,7 @@ module DependencyProxy
end
def respond(from_cache: true)
- if @manifest.persisted?
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- @manifest.touch if from_cache
-
+ if @manifest
success(manifest: @manifest, from_cache: from_cache)
else
error('Failed to download the manifest from the external registry', 503)
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index fdaf2107686..b94b14bf6bd 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -14,19 +14,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs{ class: ('border-0' if feature_project_list_filter_bar) }
- = nav_link(page: [dashboard_projects_path, root_path]) do
- = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- = _("Your projects")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count)
- = nav_link(page: starred_dashboard_projects_path) do
- = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
- = _("Starred projects")
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count)
- = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
- = link_to explore_root_path, data: {placement: 'right'} do
- = _("Explore projects")
- = render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count
+ = render 'dashboard/projects_nav'
- unless feature_project_list_filter_bar
.nav-controls
= render 'shared/projects/search_form'
diff --git a/app/views/dashboard/_projects_nav.html.haml b/app/views/dashboard/_projects_nav.html.haml
new file mode 100644
index 00000000000..b5512ea9efa
--- /dev/null
+++ b/app/views/dashboard/_projects_nav.html.haml
@@ -0,0 +1,18 @@
+- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
+
+%ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs{ class: ('gl-border-0!' if feature_project_list_filter_bar) }
+ = nav_link(page: [dashboard_projects_path, root_path]) do
+ = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
+ = _("Your projects")
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count)
+ = nav_link(page: starred_dashboard_projects_path) do
+ = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
+ = _("Starred projects")
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count)
+ = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
+ = link_to explore_root_path, data: {placement: 'right'} do
+ = _("Explore projects")
+ = nav_link(page: topics_explore_projects_path) do
+ = link_to topics_explore_projects_path, data: {placement: 'right'} do
+ = _("Explore topics")
+ = render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count
diff --git a/app/views/explore/projects/topics.html.haml b/app/views/explore/projects/topics.html.haml
new file mode 100644
index 00000000000..228304d25b6
--- /dev/null
+++ b/app/views/explore/projects/topics.html.haml
@@ -0,0 +1,12 @@
+- @hide_top_links = true
+- page_title _("Topics")
+- header_title _("Topics"), topics_explore_projects_path
+
+= render_dashboard_ultimate_trial(current_user)
+
+- if current_user
+ = render 'explore/topics/head'
+- else
+ = render 'explore/head'
+
+= render partial: 'shared/topics/list'
diff --git a/app/views/explore/topics/_head.html.haml b/app/views/explore/topics/_head.html.haml
new file mode 100644
index 00000000000..f5ee95b16c3
--- /dev/null
+++ b/app/views/explore/topics/_head.html.haml
@@ -0,0 +1,9 @@
+.page-title-holder.d-flex.align-items-center
+ %h1.page-title= _('Projects')
+
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ = render 'dashboard/projects_nav'
+ .nav-controls
+ = render 'shared/topics/search_form'
diff --git a/app/views/jira_connect/branches/new.html.haml b/app/views/jira_connect/branches/new.html.haml
index f0e34c30018..74d547e6bb8 100644
--- a/app/views/jira_connect/branches/new.html.haml
+++ b/app/views/jira_connect/branches/new.html.haml
@@ -1,5 +1,6 @@
- @hide_breadcrumbs = true
- @hide_top_links = true
+- @content_class = 'limit-container-width'
- page_title _('New branch')
.js-jira-connect-create-branch{ data: @new_branch_data }
diff --git a/app/views/shared/topics/_list.html.haml b/app/views/shared/topics/_list.html.haml
new file mode 100644
index 00000000000..ddf47261d42
--- /dev/null
+++ b/app/views/shared/topics/_list.html.haml
@@ -0,0 +1,9 @@
+- remote = local_assigns.fetch(:remote, false)
+
+- if @topics.empty?
+ = render 'shared/empty_states/topics'
+- else
+ .row.gl-mt-3
+ = render partial: 'shared/topics/topic', collection: @topics
+
+ = paginate_collection @topics, remote: remote
diff --git a/app/views/shared/topics/_topic.html.haml b/app/views/shared/topics/_topic.html.haml
new file mode 100644
index 00000000000..a47d4495777
--- /dev/null
+++ b/app/views/shared/topics/_topic.html.haml
@@ -0,0 +1,16 @@
+- max_topic_name_length = 30
+- detail_page_link = topic_explore_projects_path(topic_name: topic.name)
+
+.col-lg-3.col-md-4.col-sm-12
+ .gl-card.gl-mb-5
+ .gl-card-body.gl-display-flex.gl-align-items-center
+ .avatar-container.rect-avatar.s40.gl-flex-shrink-0
+ = link_to detail_page_link do
+ = topic_icon(topic, class: "avatar s40")
+ = link_to detail_page_link do
+ - if topic.name.length > max_topic_name_length
+ %h5.str-truncated.has-tooltip{ title: topic.name }
+ = truncate(topic.name, length: max_topic_name_length)
+ - else
+ %h5
+ = topic.name
diff --git a/config/feature_flags/development/new_customersdot_staging_url.yml b/config/feature_flags/development/dependency_proxy_manifest_workhorse.yml
index 288d7f66f01..5ff6fc93e0a 100644
--- a/config/feature_flags/development/new_customersdot_staging_url.yml
+++ b/config/feature_flags/development/dependency_proxy_manifest_workhorse.yml
@@ -1,8 +1,8 @@
---
-name: new_customersdot_staging_url
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71827
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342513
+name: dependency_proxy_manifest_workhorse
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73033
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344216
milestone: '14.4'
type: development
-group: group::fulfillment
+group: group::package
default_enabled: false
diff --git a/config/feature_flags/development/use_vsa_aggregated_tables.yml b/config/feature_flags/development/use_vsa_aggregated_tables.yml
new file mode 100644
index 00000000000..d2adec3633b
--- /dev/null
+++ b/config/feature_flags/development/use_vsa_aggregated_tables.yml
@@ -0,0 +1,8 @@
+---
+name: use_vsa_aggregated_tables
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72978
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343429
+milestone: '14.5'
+type: development
+group: group::optimize
+default_enabled: false
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
index 946b76cc485..6ddf4d23138 100644
--- a/config/routes/explore.rb
+++ b/config/routes/explore.rb
@@ -5,6 +5,7 @@ namespace :explore do
collection do
get :trending
get :starred
+ get :topics
get 'topics/:topic_name', action: :topic, as: :topic, constraints: { topic_name: /.+/ }
end
end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index c093be6db4d..9a50d580747 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -155,5 +155,7 @@ scope format: false do
get 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha' => 'groups/dependency_proxy_for_containers#blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
post 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha/upload/authorize' => 'groups/dependency_proxy_for_containers#authorize_upload_blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
post 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha/upload' => 'groups/dependency_proxy_for_containers#upload_blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
+ post 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag/upload/authorize' => 'groups/dependency_proxy_for_containers#authorize_upload_manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
+ post 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag/upload' => 'groups/dependency_proxy_for_containers#upload_manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
end
end
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index f6de760b0d1..a7e67d19de3 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -831,6 +831,17 @@ Set the limit to `0` to allow any file size.
When asking for versions of a given NuGet package name, the GitLab Package Registry returns a maximum of 300 versions.
+## Dependency Proxy Limits
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6396) in GitLab 14.5.
+
+The maximum file size for an image cached in the
+[Dependency Proxy](../user/packages/dependency_proxy/index.md)
+varies by file type:
+
+- Image blob: 5 GB
+- Image manifest: 10 MB
+
## Branch retargeting on merge
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320902) in GitLab 13.9.
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index 871dffd6140..9c3c33e1fa8 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -12,9 +12,10 @@ full list of reference architectures, see
> - **Supported users (approximate):** 10,000
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
-> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS
-> - **[Latest 10k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/10k)**
+> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
+> - **Performance tested daily with the [GitLab Performance Tool](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git (Pull): 20 RPS, Git (Push): 4 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/10k)**
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------------------|-------------|-------------------------|------------------|--------------|-----------|
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
index ae832c2226f..5488d8d33a6 100644
--- a/doc/administration/reference_architectures/1k_users.md
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -20,8 +20,9 @@ many organizations.
> follow a modified [3K reference architecture](3k_users.md#supported-modifications-for-lower-user-counts-ha).
> - **Cloud Native Hybrid:** No. For a cloud native hybrid environment, you
> can follow a [modified hybrid reference architecture](#cloud-native-hybrid-reference-architecture-with-helm-charts).
-> - **Test requests per second (RPS) rates:** API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS
-> - **[Latest 1k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/1k)**
+> - **Performance tested daily with the [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 20 RPS, Web: 2 RPS, Git (Pull): 2 RPS, Git (Push): 1 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/1k)**
| Users | Configuration | GCP | AWS | Azure |
|--------------|-------------------------|----------------|--------------|----------|
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 751a6fe4ce4..25cafbe667b 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -12,9 +12,10 @@ full list of reference architectures, see
> - **Supported users (approximate):** 25,000
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
-> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
-> - **[Latest 25k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/25k)**
+> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
+> - **Performance tested weekly with the [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/25k)**
| Service | Nodes | Configuration | GCP | AWS | Azure |
|---------------------------------------------------|-------------|-------------------------|------------------|--------------|-----------|
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index f98009b9e63..e619294704f 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -14,8 +14,9 @@ For a full list of reference architectures, see
> - **High Availability:** No. For a highly-available environment, you can
> follow a modified [3K reference architecture](3k_users.md#supported-modifications-for-lower-user-counts-ha).
> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS
-> - **[Latest 2k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/2k)**
+> - **Performance tested daily with the [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 40 RPS, Web: 4 RPS, Git (Pull): 4 RPS, Git (Push): 1 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/2k)**
| Service | Nodes | Configuration | GCP | AWS | Azure |
|------------------------------------------|--------|-------------------------|-----------------|--------------|----------|
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 32f5ea189d6..9332ae8d271 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -22,9 +22,10 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 3,000
> - **High Availability:** Yes, although [Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution
-> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
-> - **[Latest 3k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/3k)**
+> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
+> - **Performance tested weekly with the [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/3k)**
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-----------------------|-----------------|--------------|----------|
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 7c25544aaf0..bbdf798d9ad 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -12,9 +12,10 @@ full list of reference architectures, see
> - **Supported users (approximate):** 50,000
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
-> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
-> - **[Latest 50k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/50k)**
+> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
+> - **Performance tested weekly with the [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/50k)**
| Service | Nodes | Configuration | GCP | AWS | Azure |
|---------------------------------------------------|-------------|-------------------------|------------------|---------------|-----------|
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index 388c86a1511..a1921f50e4e 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -19,9 +19,10 @@ costly-to-operate environment by using the
> - **Supported users (approximate):** 5,000
> - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
-> - **Cloud Native Hybrid:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
-> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
-> - **[Latest 5k weekly performance testing results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/5k)**
+> - **Cloud Native Hybrid Alternative:** [Yes](#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative)
+> - **Performance tested weekly with the [GitLab Performance Tool (GPT)](https://gitlab.com/gitlab-org/quality/performance)**:
+> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
+> - **[Latest Results](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Benchmarks/Latest/5k)**
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
diff --git a/doc/ci/index.md b/doc/ci/index.md
index bad900cb181..5dcb0bcb242 100644
--- a/doc/ci/index.md
+++ b/doc/ci/index.md
@@ -86,10 +86,10 @@ GitLab CI/CD features, grouped by DevOps stage, include:
| [Browser Performance Testing](../user/project/merge_requests/browser_performance_testing.md) | Quickly determine the browser performance impact of pending code changes. |
| [Load Performance Testing](../user/project/merge_requests/load_performance_testing.md) | Quickly determine the server performance impact of pending code changes. |
| [CI services](services/index.md) | Link Docker containers with your base image. |
-| [Code Quality](../user/project/merge_requests/code_quality.md) | Analyze your source code quality. |
| [GitLab CI/CD for external repositories](ci_cd_for_external_repos/index.md) **(PREMIUM)** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and Bitbucket Cloud. |
| [Interactive Web Terminals](interactive_web_terminal/index.md) **(FREE SELF)** | Open an interactive web terminal to debug the running jobs. |
-| [Unit test reports](unit_test_reports.md) | Identify script failures directly on merge requests. |
+| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
+| [Unit test reports](unit_test_reports.md) | Identify test failures directly on merge requests. |
| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
|-------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------|
| **Release** | |
@@ -100,10 +100,10 @@ GitLab CI/CD features, grouped by DevOps stage, include:
| [Feature Flags](../operations/feature_flags.md) | Deploy your features behind Feature Flags. |
| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
-| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
| [Cloud deployment](cloud_deployment/index.md) | Deploy your application to a main cloud provider. |
|-------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------|
| **Secure** | |
+| [Code Quality](../user/project/merge_requests/code_quality.md) | Analyze your source code quality. |
| [Container Scanning](../user/application_security/container_scanning/index.md) **(ULTIMATE)** | Check your Docker containers for known vulnerabilities. |
| [Dependency Scanning](../user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
| [License Compliance](../user/compliance/license_compliance/index.md) **(ULTIMATE)** | Search your project dependencies for their licenses. |
@@ -148,6 +148,10 @@ See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJ
As GitLab CI/CD has evolved, certain breaking changes have
been necessary.
+#### 14.0
+
+- No breaking changes.
+
#### 13.0
- [Remove Backported `os.Expand`](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/4915).
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index bd1b96bbc9b..745d2776ead 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -2755,51 +2755,42 @@ artifacts are restored after [caches](#cache).
#### `dependencies`
-By default, all `artifacts` from previous stages
-are passed to each job. However, you can use the `dependencies` keyword to
-define a limited list of jobs to fetch artifacts from. You can also set a job to download no artifacts at all.
+Use the `dependencies` keyword to define a list of jobs to fetch artifacts from.
+You can also set a job to download no artifacts at all.
-To use this feature, define `dependencies` in context of the job and pass
-a list of all previous jobs the artifacts should be downloaded from.
+If you do not use `dependencies`, all `artifacts` from previous stages are passed to each job.
-You can define jobs from stages that were executed before the current one.
-An error occurs if you define jobs from the current or an upcoming stage.
-
-To prevent a job from downloading artifacts, define an empty array.
+**Keyword type**: Job keyword. You can use it only as part of a job.
-When you use `dependencies`, the status of the previous job is not considered.
-If a job fails or it's a manual job that isn't triggered, no error occurs.
+**Possible inputs**:
-The following example defines two jobs with artifacts: `build:osx` and
-`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
-are downloaded and extracted in the context of the build. The same happens
-for `test:linux` and artifacts from `build:linux`.
+- The names of jobs to fetch artifacts from.
+- An empty array (`[]`), to configure the job to not download any artifacts.
-The job `deploy` downloads artifacts from all previous jobs because of
-the [stage](#stages) precedence:
+**Example of `dependencies`**:
```yaml
-build:osx:
+build osx:
stage: build
script: make build:osx
artifacts:
paths:
- binaries/
-build:linux:
+build linux:
stage: build
script: make build:linux
artifacts:
paths:
- binaries/
-test:osx:
+test osx:
stage: test
script: make test:osx
dependencies:
- build:osx
-test:linux:
+test linux:
stage: test
script: make test:linux
dependencies:
@@ -2810,14 +2801,18 @@ deploy:
script: make deploy
```
-##### When a dependent job fails
+In this example, two jobs have artifacts: `build osx` and `build linux`. When `test osx` is executed,
+the artifacts from `build osx` are downloaded and extracted in the context of the build.
+The same thing happens for `test linux` and artifacts from `build linux`.
-> Introduced in GitLab 10.3.
+The `deploy` job downloads artifacts from all previous jobs because of
+the [stage](#stages) precedence.
+
+**Additional details**:
-If the artifacts of the job that is set as a dependency are
-[expired](#artifactsexpire_in) or
-[deleted](../pipelines/job_artifacts.md#delete-job-artifacts), then
-the dependent job fails.
+- The job status does not matter. If a job fails or it's a manual job that isn't triggered, no error occurs.
+- If the artifacts of a dependent job are [expired](#artifactsexpire_in) or
+ [deleted](../pipelines/job_artifacts.md#delete-job-artifacts), then the job fails.
#### `artifacts:exclude`
diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md
index 664a0361da4..482a3a7045f 100644
--- a/doc/integration/jira/dvcs.md
+++ b/doc/integration/jira/dvcs.md
@@ -86,6 +86,7 @@ you can set up this integration with your own account instead.
`https://<gitlab.example.com>/-/jira/login/oauth/callback`.
1. For **Scopes**, select `api` and clear any other checkboxes.
+ - The connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks.
1. Select **Submit**.
1. GitLab displays the generated **Application ID**
and **Secret** values. Copy these values, as you need them to configure Jira.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 0891d48c038..e0405955d3d 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -40,8 +40,14 @@ including:
## Group webhooks **(PREMIUM)**
-You can configure a webhook for a group to ensure all projects in the group
-receive the same webhook settings.
+You can configure a group webhook, which is triggered by events
+that occur across all projects in the group.
+
+Group webhooks can also be configured to listen for events that are
+specific to a group, including:
+
+- [Group member events](webhook_events.md#group-member-events)
+- [Subgroup events](webhook_events.md#subgroup-events)
## Configure a webhook
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
new file mode 100644
index 00000000000..ee459f2790a
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ # rubocop: disable CodeReuse/ActiveRecord
+ class BaseQueryBuilder
+ include StageQueryHelpers
+
+ MODEL_CLASSES = {
+ MergeRequest.to_s => ::Analytics::CycleAnalytics::MergeRequestStageEvent,
+ Issue.to_s => ::Analytics::CycleAnalytics::IssueStageEvent
+ }.freeze
+
+ # Allowed params:
+ # * from - stage end date filter start date
+ # * to - stage end date filter to date
+ # * author_username
+ # * milestone_title
+ # * label_name (array)
+ # * assignee_username (array)
+ # * project_ids (array)
+ def initialize(stage:, params: {})
+ @stage = stage
+ @params = params
+ @root_ancestor = stage.parent.root_ancestor
+ @stage_event_model = MODEL_CLASSES.fetch(stage.subject_class.to_s)
+ end
+
+ def build
+ query = base_query
+ query = filter_by_stage_parent(query)
+ query = filter_author(query)
+ query = filter_milestone_ids(query)
+ query = filter_label_names(query)
+ filter_assignees(query)
+ end
+
+ def filter_author(query)
+ return query if params[:author_username].blank?
+
+ user = User.by_username(params[:author_username]).first
+
+ return query.none if user.blank?
+
+ query.authored(user)
+ end
+
+ def filter_milestone_ids(query)
+ return query if params[:milestone_title].blank?
+
+ milestone = MilestonesFinder
+ .new(group_ids: root_ancestor.self_and_descendant_ids, project_ids: root_ancestor.all_projects.select(:id), title: params[:milestone_title])
+ .execute
+ .first
+
+ return query.none if milestone.blank?
+
+ query.with_milestone_id(milestone.id)
+ end
+
+ def filter_label_names(query)
+ return query if params[:label_name].blank?
+
+ all_label_ids = Issuables::LabelFilter
+ .new(group: root_ancestor, project: nil, params: { label_name: params[:label_name] })
+ .find_label_ids(params[:label_name])
+
+ return query.none if params[:label_name].size != all_label_ids.size
+
+ all_label_ids.each do |label_ids|
+ relation = LabelLink
+ .where(target_type: stage.subject_class.name)
+ .where(LabelLink.arel_table['target_id'].eq(query.model.arel_table[query.model.issuable_id_column]))
+
+ relation = relation.where(label_id: label_ids)
+
+ query = query.where(relation.arel.exists)
+ end
+
+ query
+ end
+
+ def filter_assignees(query)
+ return query if params[:assignee_username].blank?
+
+ Issuables::AssigneeFilter
+ .new(params: { assignee_username: params[:assignee_username] })
+ .filter(query)
+ end
+
+ def filter_by_stage_parent(query)
+ query.by_project_id(stage.parent_id)
+ end
+
+ def base_query
+ query = stage_event_model
+ .by_stage_event_hash_id(stage.stage_event_hash_id)
+
+ from = params[:from] || 30.days.ago
+ if in_progress?
+ query = query
+ .end_event_is_not_happened_yet
+ .opened_state
+ .start_event_timestamp_after(from)
+ query = query.start_event_timestamp_before(params[:to]) if params[:to]
+ else
+ query = query.end_event_timestamp_after(from)
+ query = query.end_event_timestamp_before(params[:to]) if params[:to]
+ end
+
+ query
+ end
+
+ private
+
+ attr_reader :stage, :params, :root_ancestor, :stage_event_model
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
+Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder.prepend_mod_with('Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder')
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
new file mode 100644
index 00000000000..ab3ae93f5ff
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ # Arguments:
+ # stage - an instance of CycleAnalytics::ProjectStage or CycleAnalytics::GroupStage
+ # params:
+ # current_user: an instance of User
+ # from: DateTime
+ # to: DateTime
+ class DataCollector
+ include Gitlab::Utils::StrongMemoize
+
+ MAX_COUNT = 10001
+
+ delegate :serialized_records, to: :records_fetcher
+
+ def initialize(stage:, params: {})
+ @stage = stage
+ @params = params
+ end
+
+ def median
+ strong_memoize(:median) { Median.new(stage: stage, query: query, params: params) }
+ end
+
+ def count
+ strong_memoize(:count) { limit_count }
+ end
+
+ private
+
+ attr_reader :stage, :params
+
+ def query
+ BaseQueryBuilder.new(stage: stage, params: params).build
+ end
+
+ def limit_count
+ query.limit(MAX_COUNT).count
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/median.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/median.rb
new file mode 100644
index 00000000000..181ee20948b
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/median.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ class Median
+ include StageQueryHelpers
+
+ def initialize(stage:, query:, params:)
+ @stage = stage
+ @query = query
+ @params = params
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def seconds
+ @query = @query.select(median_duration_in_seconds.as('median')).reorder(nil)
+ result = @query.take || {}
+
+ result['median'] || nil
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def days
+ seconds ? seconds.fdiv(1.day) : nil
+ end
+
+ private
+
+ attr_reader :stage, :query, :params
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb
new file mode 100644
index 00000000000..f23d1832df9
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ module StageQueryHelpers
+ def percentile_cont
+ percentile_cont_ordering = Arel::Nodes::UnaryOperation.new(Arel::Nodes::SqlLiteral.new('ORDER BY'), duration)
+ Arel::Nodes::NamedFunction.new(
+ 'percentile_cont(0.5) WITHIN GROUP',
+ [percentile_cont_ordering]
+ )
+ end
+
+ def duration
+ if in_progress?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new('TO_TIMESTAMP', [Time.current.to_i]),
+ query.model.arel_table[:start_event_timestamp]
+ )
+ else
+ Arel::Nodes::Subtraction.new(
+ query.model.arel_table[:end_event_timestamp],
+ query.model.arel_table[:start_event_timestamp]
+ )
+ end
+ end
+
+ def median_duration_in_seconds
+ Arel::Nodes::Extract.new(percentile_cont, :epoch)
+ end
+
+ def in_progress?
+ params[:end_event_filter] == :in_progress
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
index 56179533ffb..a26df55bd0a 100644
--- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb
+++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
@@ -29,7 +29,11 @@ module Gitlab
def median
strong_memoize(:median) do
- Median.new(stage: stage, query: query, params: params)
+ if use_aggregated_data_collector?
+ aggregated_data_collector.median
+ else
+ Median.new(stage: stage, query: query, params: params)
+ end
end
end
@@ -41,7 +45,11 @@ module Gitlab
def count
strong_memoize(:count) do
- limit_count
+ if use_aggregated_data_collector?
+ aggregated_data_collector.count
+ else
+ limit_count
+ end
end
end
@@ -59,6 +67,14 @@ module Gitlab
def limit_count
query.limit(MAX_COUNT).count
end
+
+ def aggregated_data_collector
+ @aggregated_data_collector ||= Aggregated::DataCollector.new(stage: stage, params: params)
+ end
+
+ def use_aggregated_data_collector?
+ params.fetch(:use_aggregated_data_collector, false)
+ end
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/request_params.rb b/lib/gitlab/analytics/cycle_analytics/request_params.rb
index 94e20762368..bc9d94ef09c 100644
--- a/lib/gitlab/analytics/cycle_analytics/request_params.rb
+++ b/lib/gitlab/analytics/cycle_analytics/request_params.rb
@@ -79,7 +79,8 @@ module Gitlab
sort: sort&.to_sym,
direction: direction&.to_sym,
page: page,
- end_event_filter: end_event_filter.to_sym
+ end_event_filter: end_event_filter.to_sym,
+ use_aggregated_data_collector: Feature.enabled?(:use_vsa_aggregated_tables, group || project, default_enabled: :yaml)
}.merge(attributes.symbolize_keys.slice(*FINDER_PARAM_NAMES))
end
diff --git a/lib/gitlab/database/load_balancing/load_balancer.rb b/lib/gitlab/database/load_balancing/load_balancer.rb
index dc6678dc93b..c9ae7d222ed 100644
--- a/lib/gitlab/database/load_balancing/load_balancer.rb
+++ b/lib/gitlab/database/load_balancing/load_balancer.rb
@@ -54,7 +54,10 @@ module Gitlab
connection = host.connection
return yield connection
rescue StandardError => error
- if serialization_failure?(error)
+ if primary_only?
+ # If we only have primary configured, retrying is pointless
+ raise error
+ elsif serialization_failure?(error)
# This error can occur when a query conflicts. See
# https://www.postgresql.org/docs/current/static/hot-standby.html#HOT-STANDBY-CONFLICT
# for more information.
diff --git a/lib/gitlab/graphql/tracers/logger_tracer.rb b/lib/gitlab/graphql/tracers/logger_tracer.rb
index 8755a1844cd..c7ba56824db 100644
--- a/lib/gitlab/graphql/tracers/logger_tracer.rb
+++ b/lib/gitlab/graphql/tracers/logger_tracer.rb
@@ -45,14 +45,12 @@ module Gitlab
::Gitlab::GraphqlLogger.info(info)
end
- def query_variables_for_logging(query)
- clean_variables(query.provided_variables)
- end
-
def clean_variables(variables)
- ActiveSupport::ParameterFilter
+ filtered = ActiveSupport::ParameterFilter
.new(::Rails.application.config.filter_parameters)
.filter(variables)
+
+ filtered&.to_s
end
end
end
diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb
index 9b6bae12057..0195de0b321 100644
--- a/lib/gitlab/subscription_portal.rb
+++ b/lib/gitlab/subscription_portal.rb
@@ -4,11 +4,7 @@ module Gitlab
module SubscriptionPortal
def self.default_subscriptions_url
if ::Gitlab.dev_or_test_env?
- if Feature.enabled?(:new_customersdot_staging_url, default_enabled: :yaml)
- 'https://customers.staging.gitlab.com'
- else
- 'https://customers.stg.gitlab.com'
- end
+ 'https://customers.staging.gitlab.com'
else
'https://customers.gitlab.com'
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c40aa2273aa..3a905a2e1c5 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -8,6 +8,7 @@ require 'uri'
module Gitlab
class Workhorse
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
+ SEND_DEPENDENCY_CONTENT_TYPE_HEADER = 'Workhorse-Proxy-Content-Type'
VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
@@ -170,9 +171,9 @@ module Gitlab
]
end
- def send_dependency(token, url)
+ def send_dependency(headers, url)
params = {
- 'Header' => { Authorization: ["Bearer #{token}"] },
+ 'Header' => headers,
'Url' => url
}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 766114b5255..ab6be03cecc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13997,6 +13997,9 @@ msgstr ""
msgid "Explore public groups"
msgstr ""
+msgid "Explore topics"
+msgstr ""
+
msgid "Export"
msgstr ""
diff --git a/rubocop/cop/qa/duplicate_testcase_link.rb b/rubocop/cop/qa/duplicate_testcase_link.rb
new file mode 100644
index 00000000000..f30768c7d80
--- /dev/null
+++ b/rubocop/cop/qa/duplicate_testcase_link.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require_relative '../../qa_helpers'
+
+module RuboCop
+ module Cop
+ module QA
+ # This cop checks for duplicate testcase links across e2e specs
+ #
+ # @example
+ #
+ # # bad
+ # it 'some test', testcase: '(...)/quality/test_cases/1892'
+ # it 'another test, testcase: '(...)/quality/test_cases/1892'
+ #
+ # # good
+ # it 'some test', testcase: '(...)/quality/test_cases/1892'
+ # it 'another test, testcase: '(...)/quality/test_cases/1894'
+ class DuplicateTestcaseLink < RuboCop::Cop::Cop
+ include QAHelpers
+
+ MESSAGE = "Don't reuse the same testcase link in different tests. Replace one of `%s`."
+
+ @testcase_set = Set.new
+
+ def_node_matcher :duplicate_testcase_link, <<~PATTERN
+ (block
+ (send nil? ...
+ ...
+ (hash
+ (pair
+ (sym :testcase)
+ (str $_))...)...)...)
+ PATTERN
+
+ def on_block(node)
+ return unless in_qa_file?(node)
+
+ duplicate_testcase_link(node) do |link|
+ break unless self.class.duplicate?(link)
+
+ add_offense(node, message: MESSAGE % link)
+ end
+ end
+
+ def self.duplicate?(link)
+ !@testcase_set.add?(link)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index 5c8e080199b..61f38e7e379 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -124,6 +124,34 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
+ shared_examples 'authorize action with permission' do
+ context 'with a valid user' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'sends Workhorse local file instructions', :aggregate_failures do
+ subject
+
+ expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(DependencyProxy::FileUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['MaximumSize']).to eq(maximum_size)
+ end
+
+ it 'sends Workhorse remote object instructions', :aggregate_failures do
+ stub_dependency_proxy_object_storage(direct_upload: true)
+
+ subject
+
+ expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to be_nil
+ expect(json_response['RemoteObject']).not_to be_nil
+ expect(json_response['MaximumSize']).to eq(maximum_size)
+ end
+ end
+ end
+
before do
allow(Gitlab.config.dependency_proxy)
.to receive(:enabled).and_return(true)
@@ -136,9 +164,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
describe 'GET #manifest' do
- let_it_be(:manifest) { create(:dependency_proxy_manifest) }
+ let_it_be(:manifest) { create(:dependency_proxy_manifest, group: group) }
let(:pull_response) { { status: :success, manifest: manifest, from_cache: false } }
+ let(:tag) { 'latest1' }
before do
allow_next_instance_of(DependencyProxy::FindOrCreateManifestService) do |instance|
@@ -146,7 +175,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
- subject { get_manifest }
+ subject { get_manifest(tag) }
context 'feature enabled' do
before do
@@ -207,11 +236,26 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'a successful manifest pull'
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
- context 'with a cache entry' do
- let(:pull_response) { { status: :success, manifest: manifest, from_cache: true } }
+ context 'with workhorse response' do
+ let(:pull_response) { { status: :success, manifest: nil, from_cache: false } }
- it_behaves_like 'returning response status', :success
- it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest_from_cache'
+ it 'returns Workhorse send-dependency instructions', :aggregate_failures do
+ subject
+
+ send_data_type, send_data = workhorse_send_data
+ header, url = send_data.values_at('Header', 'Url')
+
+ expect(send_data_type).to eq('send-dependency')
+ expect(header).to eq(
+ "Authorization" => ["Bearer abcd1234"],
+ "Accept" => ::ContainerRegistry::Client::ACCEPTED_TYPES
+ )
+ expect(url).to eq(DependencyProxy::Registry.manifest_url('alpine', tag))
+ expect(response.headers['Content-Type']).to eq('application/gzip')
+ expect(response.headers['Content-Disposition']).to eq(
+ ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: manifest.file_name)
+ )
+ end
end
end
@@ -237,8 +281,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'not found when disabled'
- def get_manifest
- get :manifest, params: { group_id: group.to_param, image: 'alpine', tag: '3.9.2' }
+ def get_manifest(tag)
+ get :manifest, params: { group_id: group.to_param, image: 'alpine', tag: tag }
end
end
@@ -383,53 +427,71 @@ RSpec.describe Groups::DependencyProxyForContainersController do
describe 'GET #authorize_upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
+ let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE }
- subject(:authorize_upload_blob) do
+ subject do
request.headers.merge!(workhorse_internal_api_request_header)
get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
end
it_behaves_like 'without permission'
+ it_behaves_like 'authorize action with permission'
+ end
+
+ describe 'GET #upload_blob' do
+ let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
+ let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
+
+ subject do
+ request.headers.merge!(workhorse_internal_api_request_header)
+
+ get :upload_blob, params: {
+ group_id: group.to_param,
+ image: 'alpine',
+ sha: blob_sha,
+ file: file
+ }
+ end
+
+ it_behaves_like 'without permission'
context 'with a valid user' do
before do
group.add_guest(user)
- end
- it 'sends Workhorse local file instructions', :aggregate_failures do
- authorize_upload_blob
-
- expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to eq(DependencyProxy::FileUploader.workhorse_local_upload_path)
- expect(json_response['RemoteObject']).to be_nil
- expect(json_response['MaximumSize']).to eq(5.gigabytes)
+ expect_next_found_instance_of(Group) do |instance|
+ expect(instance).to receive_message_chain(:dependency_proxy_blobs, :create!)
+ end
end
- it 'sends Workhorse remote object instructions', :aggregate_failures do
- stub_dependency_proxy_object_storage(direct_upload: true)
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
+ end
+ end
- authorize_upload_blob
+ describe 'GET #authorize_upload_manifest' do
+ let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE }
- expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to be_nil
- expect(json_response['RemoteObject']).not_to be_nil
- expect(json_response['MaximumSize']).to eq(5.gigabytes)
- end
+ subject do
+ request.headers.merge!(workhorse_internal_api_request_header)
+
+ get :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
end
+
+ it_behaves_like 'without permission'
+ it_behaves_like 'authorize action with permission'
end
- describe 'GET #upload_blob' do
- let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
- let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
+ describe 'GET #upload_manifest' do
+ let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
- get :upload_blob, params: {
+ get :upload_manifest, params: {
group_id: group.to_param,
image: 'alpine',
- sha: blob_sha,
+ tag: 'latest',
file: file
}
end
@@ -441,11 +503,11 @@ RSpec.describe Groups::DependencyProxyForContainersController do
group.add_guest(user)
expect_next_found_instance_of(Group) do |instance|
- expect(instance).to receive_message_chain(:dependency_proxy_blobs, :create!)
+ expect(instance).to receive_message_chain(:dependency_proxy_manifests, :create!)
end
end
- it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
end
end
diff --git a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
index 1351ba35a71..3f0318c3973 100644
--- a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
+++ b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
end
before do
+ stub_feature_flags(use_vsa_aggregated_tables: false)
sign_in(user)
end
diff --git a/spec/factories/analytics/cycle_analytics/issue_stage_events.rb b/spec/factories/analytics/cycle_analytics/issue_stage_events.rb
new file mode 100644
index 00000000000..8ad88152611
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/issue_stage_events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_issue_stage_event, class: 'Analytics::CycleAnalytics::IssueStageEvent' do
+ sequence(:stage_event_hash_id) { |n| n }
+ sequence(:issue_id) { 0 }
+ sequence(:group_id) { 0 }
+ sequence(:project_id) { 0 }
+
+ start_event_timestamp { 3.weeks.ago.to_date }
+ end_event_timestamp { 2.weeks.ago.to_date }
+ end
+end
diff --git a/spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb b/spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb
new file mode 100644
index 00000000000..d8fa43b024f
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_merge_request_stage_event, class: 'Analytics::CycleAnalytics::MergeRequestStageEvent' do
+ sequence(:stage_event_hash_id) { |n| n }
+ sequence(:merge_request_id) { 0 }
+ sequence(:group_id) { 0 }
+ sequence(:project_id) { 0 }
+
+ start_event_timestamp { 3.weeks.ago.to_date }
+ end_event_timestamp { 2.weeks.ago.to_date }
+ end
+end
diff --git a/spec/features/explore/topics_spec.rb b/spec/features/explore/topics_spec.rb
new file mode 100644
index 00000000000..9d2e76bc3a1
--- /dev/null
+++ b/spec/features/explore/topics_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Explore Topics' do
+ context 'when no topics exist' do
+ it 'renders empty message', :aggregate_failures do
+ visit topics_explore_projects_path
+
+ expect(current_path).to eq topics_explore_projects_path
+ expect(page).to have_content('There are no topics to show.')
+ end
+ end
+
+ context 'when topics exist' do
+ let!(:topic) { create(:topic, name: 'topic1') }
+
+ it 'renders topic list' do
+ visit topics_explore_projects_path
+
+ expect(current_path).to eq topics_explore_projects_path
+ expect(page).to have_content('topic1')
+ end
+ end
+end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index da7e5d5dce2..10bd45e3189 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -188,6 +188,11 @@ RSpec.describe Nav::TopNavHelper do
href: '/explore',
id: 'explore',
title: 'Explore projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore/projects/topics',
+ id: 'topics',
+ title: 'Explore topics'
)
]
expect(projects_view[:linksPrimary]).to eq(expected_links_primary)
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
new file mode 100644
index 00000000000..bf2f8d8159b
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:user_1) { create(:user) }
+
+ let_it_be(:label_1) { create(:label, project: project) }
+ let_it_be(:label_2) { create(:label, project: project) }
+
+ let_it_be(:issue_1) { create(:issue, project: project, author: project.creator, labels: [label_1, label_2]) }
+ let_it_be(:issue_2) { create(:issue, project: project, milestone: milestone, assignees: [user_1]) }
+ let_it_be(:issue_3) { create(:issue, project: project) }
+ let_it_be(:issue_outside_project) { create(:issue) }
+
+ let_it_be(:stage) do
+ create(:cycle_analytics_project_stage,
+ project: project,
+ start_event_identifier: :issue_created,
+ end_event_identifier: :issue_deployed_to_production
+ )
+ end
+
+ let_it_be(:stage_event_1) do
+ create(:cycle_analytics_issue_stage_event,
+ stage_event_hash_id: stage.stage_event_hash_id,
+ group_id: group.id,
+ project_id: project.id,
+ issue_id: issue_1.id,
+ author_id: project.creator.id,
+ milestone_id: nil,
+ state_id: issue_1.state_id,
+ end_event_timestamp: 8.months.ago
+ )
+ end
+
+ let_it_be(:stage_event_2) do
+ create(:cycle_analytics_issue_stage_event,
+ stage_event_hash_id: stage.stage_event_hash_id,
+ group_id: group.id,
+ project_id: project.id,
+ issue_id: issue_2.id,
+ author_id: nil,
+ milestone_id: milestone.id,
+ state_id: issue_2.state_id
+ )
+ end
+
+ let_it_be(:stage_event_3) do
+ create(:cycle_analytics_issue_stage_event,
+ stage_event_hash_id: stage.stage_event_hash_id,
+ group_id: group.id,
+ project_id: project.id,
+ issue_id: issue_3.id,
+ author_id: nil,
+ milestone_id: milestone.id,
+ state_id: issue_3.state_id,
+ start_event_timestamp: 8.months.ago,
+ end_event_timestamp: nil
+ )
+ end
+
+ let(:params) do
+ {
+ from: 1.year.ago.to_date,
+ to: Date.today
+ }
+ end
+
+ subject(:issue_ids) { described_class.new(stage: stage, params: params).build.pluck(:issue_id) }
+
+ it 'scopes the query for the given project' do
+ expect(issue_ids).to match_array([issue_1.id, issue_2.id])
+ expect(issue_ids).not_to include([issue_outside_project.id])
+ end
+
+ describe 'author_username param' do
+ it 'returns stage events associated with the given author' do
+ params[:author_username] = project.creator.username
+
+ expect(issue_ids).to eq([issue_1.id])
+ end
+
+ it 'returns empty result when unknown author is given' do
+ params[:author_username] = 'no one'
+
+ expect(issue_ids).to be_empty
+ end
+ end
+
+ describe 'milestone_title param' do
+ it 'returns stage events associated with the milestone' do
+ params[:milestone_title] = milestone.title
+
+ expect(issue_ids).to eq([issue_2.id])
+ end
+
+ it 'returns empty result when unknown milestone is given' do
+ params[:milestone_title] = 'unknown milestone'
+
+ expect(issue_ids).to be_empty
+ end
+ end
+
+ describe 'label_name param' do
+ it 'returns stage events associated with multiple labels' do
+ params[:label_name] = [label_1.name, label_2.name]
+
+ expect(issue_ids).to eq([issue_1.id])
+ end
+
+ it 'does not include records with partial label match' do
+ params[:label_name] = [label_1.name, 'other label']
+
+ expect(issue_ids).to be_empty
+ end
+ end
+
+ describe 'assignee_username param' do
+ it 'returns stage events associated assignee' do
+ params[:assignee_username] = [user_1.username]
+
+ expect(issue_ids).to eq([issue_2.id])
+ end
+ end
+
+ describe 'timestamp filtering' do
+ before do
+ params[:from] = 1.year.ago
+ params[:to] = 6.months.ago
+ end
+
+ it 'filters by the end event time range' do
+ expect(issue_ids).to eq([issue_1.id])
+ end
+
+ context 'when in_progress items are requested' do
+ before do
+ params[:end_event_filter] = :in_progress
+ end
+
+ it 'filters by the start event time range' do
+ expect(issue_ids).to eq([issue_3.id])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
index 883c8473704..117fa6be209 100644
--- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
@@ -141,6 +141,24 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
lb.read { raise conflict_error }
end
+ context 'only primary is configured' do
+ let(:lb) do
+ config = Gitlab::Database::LoadBalancing::Configuration.new(ActiveRecord::Base)
+ allow(config).to receive(:load_balancing_enabled?).and_return(false)
+
+ described_class.new(config)
+ end
+
+ it 'does not retry a query on connection error if only the primary is configured' do
+ host = double(:host, query_cache_enabled: true)
+
+ allow(lb).to receive(:host).and_return(host)
+ allow(host).to receive(:connection).and_raise(PG::UnableToSend)
+
+ expect { lb.read }.to raise_error(PG::UnableToSend)
+ end
+ end
+
it 'uses the primary if no secondaries are available' do
allow(lb).to receive(:connection_error?).and_return(true)
diff --git a/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb
index d6aea36268d..d83ac4dabc5 100644
--- a/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb
+++ b/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Graphql::Tracers::LoggerTracer do
query_fingerprint: query.fingerprint,
query_string: query_string,
trace_type: "execute_query",
- variables: variables
+ variables: variables.to_s
})
dummy_schema.execute(query_string, variables: variables)
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index a3808b0f0e2..4fa9ee7afb0 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -9,14 +9,13 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
before do
stub_env('CUSTOMER_PORTAL_URL', env_value)
- stub_feature_flags(new_customersdot_staging_url: false)
end
describe '.default_subscriptions_url' do
where(:test, :development, :result) do
false | false | 'https://customers.gitlab.com'
- false | true | 'https://customers.stg.gitlab.com'
- true | false | 'https://customers.stg.gitlab.com'
+ false | true | 'https://customers.staging.gitlab.com'
+ true | false | 'https://customers.staging.gitlab.com'
end
before do
@@ -35,7 +34,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
subject { described_class.subscriptions_url }
context 'when CUSTOMER_PORTAL_URL ENV is unset' do
- it { is_expected.to eq('https://customers.stg.gitlab.com') }
+ it { is_expected.to eq('https://customers.staging.gitlab.com') }
end
context 'when CUSTOMER_PORTAL_URL ENV is set' do
@@ -55,15 +54,15 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
context 'url methods' do
where(:method_name, :result) do
- :default_subscriptions_url | 'https://customers.stg.gitlab.com'
- :payment_form_url | 'https://customers.stg.gitlab.com/payment_forms/cc_validation'
- :subscriptions_graphql_url | 'https://customers.stg.gitlab.com/graphql'
- :subscriptions_more_minutes_url | 'https://customers.stg.gitlab.com/buy_pipeline_minutes'
- :subscriptions_more_storage_url | 'https://customers.stg.gitlab.com/buy_storage'
- :subscriptions_manage_url | 'https://customers.stg.gitlab.com/subscriptions'
- :subscriptions_plans_url | 'https://customers.stg.gitlab.com/plans'
- :subscriptions_instance_review_url | 'https://customers.stg.gitlab.com/instance_review'
- :subscriptions_gitlab_plans_url | 'https://customers.stg.gitlab.com/gitlab_plans'
+ :default_subscriptions_url | 'https://customers.staging.gitlab.com'
+ :payment_form_url | 'https://customers.staging.gitlab.com/payment_forms/cc_validation'
+ :subscriptions_graphql_url | 'https://customers.staging.gitlab.com/graphql'
+ :subscriptions_more_minutes_url | 'https://customers.staging.gitlab.com/buy_pipeline_minutes'
+ :subscriptions_more_storage_url | 'https://customers.staging.gitlab.com/buy_storage'
+ :subscriptions_manage_url | 'https://customers.staging.gitlab.com/subscriptions'
+ :subscriptions_plans_url | 'https://customers.staging.gitlab.com/plans'
+ :subscriptions_instance_review_url | 'https://customers.staging.gitlab.com/instance_review'
+ :subscriptions_gitlab_plans_url | 'https://customers.staging.gitlab.com/gitlab_plans'
end
with_them do
@@ -78,7 +77,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
let(:group_id) { 153 }
- it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/extra_seats") }
+ it { is_expected.to eq("https://customers.staging.gitlab.com/gitlab/namespaces/#{group_id}/extra_seats") }
end
describe '.upgrade_subscription_url' do
@@ -87,7 +86,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
let(:group_id) { 153 }
let(:plan_id) { 5 }
- it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/upgrade/#{plan_id}") }
+ it { is_expected.to eq("https://customers.staging.gitlab.com/gitlab/namespaces/#{group_id}/upgrade/#{plan_id}") }
end
describe '.renew_subscription_url' do
@@ -95,6 +94,6 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
let(:group_id) { 153 }
- it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/renew") }
+ it { is_expected.to eq("https://customers.staging.gitlab.com/gitlab/namespaces/#{group_id}/renew") }
end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 8ba56af561d..3bab9aec454 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -512,6 +512,24 @@ RSpec.describe Gitlab::Workhorse do
end
end
+ describe '.send_dependency' do
+ let(:headers) { { Accept: 'foo', Authorization: 'Bearer asdf1234' } }
+ let(:url) { 'https://foo.bar.com/baz' }
+
+ subject { described_class.send_dependency(headers, url) }
+
+ it 'sets the header correctly', :aggregate_failures do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("send-dependency")
+ expect(params).to eq({
+ 'Header' => headers,
+ 'Url' => url
+ }.deep_stringify_keys)
+ end
+ end
+
describe '.send_git_snapshot' do
let(:url) { 'http://example.com' }
diff --git a/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb b/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
index 7201401fa38..ac17271ff99 100644
--- a/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
+++ b/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
@@ -13,5 +13,8 @@ RSpec.describe Analytics::CycleAnalytics::IssueStageEvent do
expect(described_class.states).to eq(Issue.available_states)
end
- it_behaves_like 'StageEventModel'
+ it_behaves_like 'StageEventModel' do
+ let_it_be(:stage_event_factory) { :cycle_analytics_issue_stage_event }
+ let_it_be(:issuable_factory) { :issue }
+ end
end
diff --git a/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb b/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
index 859b308eda6..bccc485d3f9 100644
--- a/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
+++ b/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
@@ -13,5 +13,8 @@ RSpec.describe Analytics::CycleAnalytics::MergeRequestStageEvent do
expect(described_class.states).to eq(MergeRequest.available_states)
end
- it_behaves_like 'StageEventModel'
+ it_behaves_like 'StageEventModel' do
+ let_it_be(:stage_event_factory) { :cycle_analytics_merge_request_stage_event }
+ let_it_be(:issuable_factory) { :merge_request }
+ end
end
diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb
index e7f0889345a..4629ff4b5b3 100644
--- a/spec/models/dependency_proxy/manifest_spec.rb
+++ b/spec/models/dependency_proxy/manifest_spec.rb
@@ -31,18 +31,14 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
end
end
- describe '.find_or_initialize_by_file_name_or_digest' do
+ describe '.find_by_file_name_or_digest' do
let_it_be(:file_name) { 'foo' }
let_it_be(:digest) { 'bar' }
- subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) }
+ subject { DependencyProxy::Manifest.find_by_file_name_or_digest(file_name: file_name, digest: digest) }
context 'no manifest exists' do
- it 'initializes a manifest' do
- expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name, digest: digest)
-
- subject
- end
+ it { is_expected.to be_nil }
end
context 'manifest exists and matches file_name' do
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 2779eecae62..d03441b0d4c 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'GraphQL' do
# operation_fingerprint starts with operation name
operation_fingerprint: %r{^anonymous\/},
is_mutation: false,
- variables: variables,
+ variables: variables.to_s,
query_string: query
}
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 35ce942ed7e..ab0c76397e4 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -517,11 +517,15 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
let(:path) { "/v2/#{group.path}/dependency_proxy/containers/alpine/manifests/latest" }
let(:other_path) { "/v2/#{other_group.path}/dependency_proxy/containers/alpine/manifests/latest" }
let(:pull_response) { { status: :success, manifest: manifest, from_cache: false } }
+ let(:head_response) { { status: :success } }
before do
allow_next_instance_of(DependencyProxy::FindOrCreateManifestService) do |instance|
allow(instance).to receive(:execute).and_return(pull_response)
end
+ allow_next_instance_of(DependencyProxy::HeadManifestService) do |instance|
+ allow(instance).to receive(:execute).and_return(head_response)
+ end
end
it_behaves_like 'rate-limited token-authenticated requests'
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index f171c2faf5e..5c2ef62683e 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -85,6 +85,26 @@ RSpec.describe "Groups", "routing" do
expect(get('/v2')).to route_to('groups/dependency_proxy_auth#authenticate')
end
+ it 'routes to #upload_manifest' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/manifests/latest/upload'))
+ .to route_to('groups/dependency_proxy_for_containers#upload_manifest', group_id: 'gitlabhq', image: 'alpine', tag: 'latest')
+ end
+
+ it 'routes to #upload_blob' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/blobs/abc12345/upload'))
+ .to route_to('groups/dependency_proxy_for_containers#upload_blob', group_id: 'gitlabhq', image: 'alpine', sha: 'abc12345')
+ end
+
+ it 'routes to #upload_manifest_authorize' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/manifests/latest/upload/authorize'))
+ .to route_to('groups/dependency_proxy_for_containers#authorize_upload_manifest', group_id: 'gitlabhq', image: 'alpine', tag: 'latest')
+ end
+
+ it 'routes to #upload_blob_authorize' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/blobs/abc12345/upload/authorize'))
+ .to route_to('groups/dependency_proxy_for_containers#authorize_upload_blob', group_id: 'gitlabhq', image: 'alpine', sha: 'abc12345')
+ end
+
context 'image name without namespace' do
it 'routes to #manifest' do
expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6'))
diff --git a/spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb b/spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb
new file mode 100644
index 00000000000..fb424da90e8
--- /dev/null
+++ b/spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/qa/duplicate_testcase_link'
+
+RSpec.describe RuboCop::Cop::QA::DuplicateTestcaseLink do
+ let(:source_file) { 'qa/page.rb' }
+
+ subject(:cop) { described_class.new }
+
+ context 'in a QA file' do
+ before do
+ allow(cop).to receive(:in_qa_file?).and_return(true)
+ end
+
+ it "registers an offense for a duplicate testcase link" do
+ expect_offense(<<-RUBY)
+ it 'some test', testcase: '/quality/test_cases/1892' do
+ end
+ it 'another test', testcase: '/quality/test_cases/1892' do
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't reuse the same testcase link in different tests. Replace one of `/quality/test_cases/1892`.
+ end
+ RUBY
+ end
+
+ it "doesnt offend if testcase link is unique" do
+ expect_no_offenses(<<-RUBY)
+ it 'some test', testcase: '/quality/test_cases/1893' do
+ end
+ it 'another test', testcase: '/quality/test_cases/1894' do
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
index b3f88f91289..f2a605756fb 100644
--- a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
@@ -31,6 +31,14 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
end
end
+ shared_examples 'returning no manifest' do
+ it 'returns a nil manifest' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:from_cache]).to eq false
+ expect(subject[:manifest]).to be_nil
+ end
+ end
+
context 'when no manifest exists' do
let_it_be(:image) { 'new-image' }
@@ -40,7 +48,15 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: headers)
end
- it_behaves_like 'downloading the manifest'
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
end
context 'failed head request' do
@@ -49,7 +65,15 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: headers)
end
- it_behaves_like 'downloading the manifest'
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
end
end
@@ -60,7 +84,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
shared_examples 'using the cached manifest' do
it 'uses cached manifest instead of downloading one', :aggregate_failures do
- expect { subject }.to change { dependency_proxy_manifest.reload.updated_at }
+ subject
expect(subject[:status]).to eq(:success)
expect(subject[:manifest]).to be_a(DependencyProxy::Manifest)
@@ -80,12 +104,20 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
end
- it 'downloads the new manifest and updates the existing record', :aggregate_failures do
- expect(subject[:status]).to eq(:success)
- expect(subject[:manifest]).to eq(dependency_proxy_manifest)
- expect(subject[:manifest].content_type).to eq(content_type)
- expect(subject[:manifest].digest).to eq(digest)
- expect(subject[:from_cache]).to eq false
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it 'downloads the new manifest and updates the existing record', :aggregate_failures do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:manifest]).to eq(dependency_proxy_manifest)
+ expect(subject[:manifest].content_type).to eq(content_type)
+ expect(subject[:manifest].digest).to eq(digest)
+ expect(subject[:from_cache]).to eq false
+ end
end
end
@@ -96,7 +128,15 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: headers)
end
- it_behaves_like 'downloading the manifest'
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
end
context 'failed connection' do
diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
index 9292b4e650b..b853d517d83 100644
--- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
+++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
@@ -74,4 +74,108 @@ RSpec.shared_examples 'StageEventModel' do
expect(input_data.map(&:values).sort).to eq(output_data)
end
end
+
+ describe 'scopes' do
+ def attributes(array)
+ array.map(&:attributes)
+ end
+
+ RSpec::Matchers.define :match_attributes do |expected|
+ match do |actual|
+ actual.map(&:attributes) == expected.map(&:attributes)
+ end
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:user) }
+ let_it_be(:milestone) { create(:milestone) }
+ let_it_be(:issuable_with_assignee) { create(issuable_factory, assignees: [user])}
+
+ let_it_be(:record) { create(stage_event_factory, start_event_timestamp: 3.years.ago.to_date, end_event_timestamp: 2.years.ago.to_date) }
+ let_it_be(:record_with_author) { create(stage_event_factory, author_id: user.id) }
+ let_it_be(:record_with_project) { create(stage_event_factory, project_id: project.id) }
+ let_it_be(:record_with_group) { create(stage_event_factory, group_id: project.namespace_id) }
+ let_it_be(:record_with_assigned_issuable) { create(stage_event_factory, described_class.issuable_id_column => issuable_with_assignee.id) }
+ let_it_be(:record_with_milestone) { create(stage_event_factory, milestone_id: milestone.id) }
+
+ it 'filters by stage_event_hash_id' do
+ records = described_class.by_stage_event_hash_id(record.stage_event_hash_id)
+
+ expect(records).to match_attributes([record])
+ end
+
+ it 'filters by project_id' do
+ records = described_class.by_project_id(project.id)
+
+ expect(records).to match_attributes([record_with_project])
+ end
+
+ it 'filters by group_id' do
+ records = described_class.by_group_id(project.namespace_id)
+
+ expect(records).to match_attributes([record_with_group])
+ end
+
+ it 'filters by author_id' do
+ records = described_class.authored(user)
+
+ expect(records).to match_attributes([record_with_author])
+ end
+
+ it 'filters by assignee' do
+ records = described_class.assigned_to(user)
+
+ expect(records).to match_attributes([record_with_assigned_issuable])
+ end
+
+ it 'filters by milestone_id' do
+ records = described_class.with_milestone_id(milestone.id)
+
+ expect(records).to match_attributes([record_with_milestone])
+ end
+
+ describe 'start_event_timestamp filtering' do
+ it 'when range is given' do
+ records = described_class
+ .start_event_timestamp_after(4.years.ago)
+ .start_event_timestamp_before(2.years.ago)
+
+ expect(records).to match_attributes([record])
+ end
+
+ it 'when specifying upper bound' do
+ records = described_class.start_event_timestamp_before(2.years.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+
+ it 'when specifying the lower bound' do
+ records = described_class.start_event_timestamp_after(4.years.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+ end
+
+ describe 'end_event_timestamp filtering' do
+ it 'when range is given' do
+ records = described_class
+ .end_event_timestamp_after(3.years.ago)
+ .end_event_timestamp_before(1.year.ago)
+
+ expect(records).to match_attributes([record])
+ end
+
+ it 'when specifying upper bound' do
+ records = described_class.end_event_timestamp_before(1.year.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+
+ it 'when specifying the lower bound' do
+ records = described_class.end_event_timestamp_after(3.years.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+ end
+ end
end
diff --git a/workhorse/internal/dependencyproxy/dependencyproxy.go b/workhorse/internal/dependencyproxy/dependencyproxy.go
index 0bba2610d9e..90f3042a342 100644
--- a/workhorse/internal/dependencyproxy/dependencyproxy.go
+++ b/workhorse/internal/dependencyproxy/dependencyproxy.go
@@ -75,6 +75,19 @@ func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData strin
helper.Fail500(w, r, fmt.Errorf("dependency proxy: failed to create request: %w", err))
}
saveFileRequest.Header = helper.HeaderClone(r.Header)
+
+ // forward headers from dependencyResponse to rails and client
+ for key, values := range dependencyResponse.Header {
+ saveFileRequest.Header.Del(key)
+ w.Header().Del(key)
+ for _, value := range values {
+ saveFileRequest.Header.Add(key, value)
+ w.Header().Add(key, value)
+ }
+ }
+
+ // workhorse hijack overwrites the Content-Type header, but we need this header value
+ saveFileRequest.Header.Set("Workhorse-Proxy-Content-Type", dependencyResponse.Header.Get("Content-Type"))
saveFileRequest.ContentLength = dependencyResponse.ContentLength
nrw := &nullResponseWriter{header: make(http.Header)}
diff --git a/workhorse/internal/dependencyproxy/dependencyproxy_test.go b/workhorse/internal/dependencyproxy/dependencyproxy_test.go
index 657ea388e18..d9169b2b4ce 100644
--- a/workhorse/internal/dependencyproxy/dependencyproxy_test.go
+++ b/workhorse/internal/dependencyproxy/dependencyproxy_test.go
@@ -113,8 +113,14 @@ func TestInject(t *testing.T) {
func TestSuccessfullRequest(t *testing.T) {
content := []byte("result")
contentLength := strconv.Itoa(len(content))
+ contentType := "foo"
+ dockerContentDigest := "sha256:asdf1234"
+ overriddenHeader := "originResourceServer"
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", contentLength)
+ w.Header().Set("Content-Type", contentType)
+ w.Header().Set("Docker-Content-Digest", dockerContentDigest)
+ w.Header().Set("Overridden-Header", overriddenHeader)
w.Write(content)
}))
@@ -131,12 +137,16 @@ func TestSuccessfullRequest(t *testing.T) {
require.Equal(t, "/target/upload", uploadHandler.request.URL.Path)
require.Equal(t, int64(6), uploadHandler.request.ContentLength)
+ require.Equal(t, contentType, uploadHandler.request.Header.Get("Workhorse-Proxy-Content-Type"))
+ require.Equal(t, dockerContentDigest, uploadHandler.request.Header.Get("Docker-Content-Digest"))
+ require.Equal(t, overriddenHeader, uploadHandler.request.Header.Get("Overridden-Header"))
require.Equal(t, content, uploadHandler.body)
require.Equal(t, 200, response.Code)
require.Equal(t, string(content), response.Body.String())
require.Equal(t, contentLength, response.Header().Get("Content-Length"))
+ require.Equal(t, dockerContentDigest, response.Header().Get("Docker-Content-Digest"))
}
func TestIncorrectSendData(t *testing.T) {
@@ -177,6 +187,7 @@ func TestFailedOriginServer(t *testing.T) {
func makeRequest(injector *Injector, data string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/target", nil)
+ r.Header.Set("Overridden-Header", "request")
sendData := base64.StdEncoding.EncodeToString([]byte(data))
injector.Inject(w, r, sendData)