mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-15 21:39:00 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -3825,6 +3825,7 @@ Gitlab/BoundedContexts:
|
||||
- 'lib/feature/actor_wrapper.rb'
|
||||
- 'lib/feature/definition.rb'
|
||||
- 'lib/feature/gitaly.rb'
|
||||
- 'lib/feature/kas.rb'
|
||||
- 'lib/feature/logger.rb'
|
||||
- 'lib/feature/shared.rb'
|
||||
- 'lib/file_size_validator.rb'
|
||||
|
@ -106,11 +106,6 @@ InternalAffairs/CopDescriptionWithExample:
|
||||
- 'rubocop/cop/migration/versioned_migration_class.rb'
|
||||
- 'rubocop/cop/migration/with_lock_retries_disallowed_method.rb'
|
||||
- 'rubocop/cop/migration/with_lock_retries_with_change.rb'
|
||||
- 'rubocop/cop/performance/active_record_subtransaction_methods.rb'
|
||||
- 'rubocop/cop/performance/active_record_subtransactions.rb'
|
||||
- 'rubocop/cop/performance/ar_count_each.rb'
|
||||
- 'rubocop/cop/performance/ar_exists_and_present_blank.rb'
|
||||
- 'rubocop/cop/performance/readlines_each.rb'
|
||||
- 'rubocop/cop/project_path_helper.rb'
|
||||
- 'rubocop/cop/put_group_routes_under_scope.rb'
|
||||
- 'rubocop/cop/put_project_routes_under_scope.rb'
|
||||
|
2
Gemfile
2
Gemfile
@ -226,7 +226,7 @@ gem 'google-apis-container_v1', '~> 0.100.0', feature_category: :shared
|
||||
gem 'google-apis-container_v1beta1', '~> 0.43.0', feature_category: :shared
|
||||
gem 'google-apis-cloudbilling_v1', '~> 0.22.0', feature_category: :shared
|
||||
gem 'google-apis-cloudresourcemanager_v1', '~> 0.31.0', feature_category: :shared
|
||||
gem 'google-apis-iam_v1', '~> 0.72.0', feature_category: :shared
|
||||
gem 'google-apis-iam_v1', '~> 0.73.0', feature_category: :shared
|
||||
gem 'google-apis-serviceusage_v1', '~> 0.28.0', feature_category: :shared
|
||||
gem 'google-apis-sqladmin_v1beta4', '~> 0.41.0', feature_category: :shared
|
||||
gem 'google-apis-androidpublisher_v3', '~> 0.84.0', feature_category: :shared
|
||||
|
@ -254,7 +254,7 @@
|
||||
{"name":"google-apis-container_v1beta1","version":"0.43.0","platform":"ruby","checksum":"68c48fcf88db926ceab16f56890c85890269e6366b272fcde958a9b5550313d0"},
|
||||
{"name":"google-apis-core","version":"0.18.0","platform":"ruby","checksum":"96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee"},
|
||||
{"name":"google-apis-dns_v1","version":"0.36.0","platform":"ruby","checksum":"5dd273d78ab37d03d1bc07837186f79ad0399e9f2b8b1ec2629ed682ea347d47"},
|
||||
{"name":"google-apis-iam_v1","version":"0.72.0","platform":"ruby","checksum":"0358e4187bbf94676a83f3b72b01045e9d0ca0cdad7cf4f06d297044c8feb4d4"},
|
||||
{"name":"google-apis-iam_v1","version":"0.73.0","platform":"ruby","checksum":"6f181165f161dd4d53e98c412d345d262114b2e26ef790d57a754f1fcf436a49"},
|
||||
{"name":"google-apis-iamcredentials_v1","version":"0.15.0","platform":"ruby","checksum":"e9a256a6d80fbfc77d44bd7e65bc94b9e1e9863a00e6d413edc0102d6cb5551b"},
|
||||
{"name":"google-apis-monitoring_v3","version":"0.54.0","platform":"ruby","checksum":"677fe1dce5b4cc937813303b020962fffb86f50a1f61f6422516937b5ad46128"},
|
||||
{"name":"google-apis-pubsub_v1","version":"0.45.0","platform":"ruby","checksum":"1dfe4614c781250a0d4491be43e134936d5c08adc75a843e27d4bb66ba3cb205"},
|
||||
|
@ -851,7 +851,7 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-dns_v1 (0.36.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-iam_v1 (0.72.0)
|
||||
google-apis-iam_v1 (0.73.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.15.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
@ -2192,7 +2192,7 @@ DEPENDENCIES
|
||||
google-apis-container_v1 (~> 0.100.0)
|
||||
google-apis-container_v1beta1 (~> 0.43.0)
|
||||
google-apis-core (~> 0.18.0, >= 0.18.0)
|
||||
google-apis-iam_v1 (~> 0.72.0)
|
||||
google-apis-iam_v1 (~> 0.73.0)
|
||||
google-apis-serviceusage_v1 (~> 0.28.0)
|
||||
google-apis-sqladmin_v1beta4 (~> 0.41.0)
|
||||
google-apis-storage_v1 (~> 0.29)
|
||||
|
@ -254,7 +254,7 @@
|
||||
{"name":"google-apis-container_v1beta1","version":"0.43.0","platform":"ruby","checksum":"68c48fcf88db926ceab16f56890c85890269e6366b272fcde958a9b5550313d0"},
|
||||
{"name":"google-apis-core","version":"0.18.0","platform":"ruby","checksum":"96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee"},
|
||||
{"name":"google-apis-dns_v1","version":"0.36.0","platform":"ruby","checksum":"5dd273d78ab37d03d1bc07837186f79ad0399e9f2b8b1ec2629ed682ea347d47"},
|
||||
{"name":"google-apis-iam_v1","version":"0.72.0","platform":"ruby","checksum":"0358e4187bbf94676a83f3b72b01045e9d0ca0cdad7cf4f06d297044c8feb4d4"},
|
||||
{"name":"google-apis-iam_v1","version":"0.73.0","platform":"ruby","checksum":"6f181165f161dd4d53e98c412d345d262114b2e26ef790d57a754f1fcf436a49"},
|
||||
{"name":"google-apis-iamcredentials_v1","version":"0.15.0","platform":"ruby","checksum":"e9a256a6d80fbfc77d44bd7e65bc94b9e1e9863a00e6d413edc0102d6cb5551b"},
|
||||
{"name":"google-apis-monitoring_v3","version":"0.54.0","platform":"ruby","checksum":"677fe1dce5b4cc937813303b020962fffb86f50a1f61f6422516937b5ad46128"},
|
||||
{"name":"google-apis-pubsub_v1","version":"0.45.0","platform":"ruby","checksum":"1dfe4614c781250a0d4491be43e134936d5c08adc75a843e27d4bb66ba3cb205"},
|
||||
|
@ -845,7 +845,7 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-dns_v1 (0.36.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-iam_v1 (0.72.0)
|
||||
google-apis-iam_v1 (0.73.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.15.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
@ -2187,7 +2187,7 @@ DEPENDENCIES
|
||||
google-apis-container_v1 (~> 0.100.0)
|
||||
google-apis-container_v1beta1 (~> 0.43.0)
|
||||
google-apis-core (~> 0.18.0, >= 0.18.0)
|
||||
google-apis-iam_v1 (~> 0.72.0)
|
||||
google-apis-iam_v1 (~> 0.73.0)
|
||||
google-apis-serviceusage_v1 (~> 0.28.0)
|
||||
google-apis-sqladmin_v1beta4 (~> 0.41.0)
|
||||
google-apis-storage_v1 (~> 0.29)
|
||||
|
@ -257,7 +257,13 @@ module Ci
|
||||
#
|
||||
# TODO consider using callbacks and state machine to remove old data
|
||||
#
|
||||
unsafe_set_data!(current_data)
|
||||
saved = unsafe_set_data!(current_data)
|
||||
|
||||
# We've seen save! fail silently so don't delete the redis data to allow
|
||||
# a self-heal to happen automatically the next run, raise to re-try
|
||||
if Feature.enabled?(:self_heal_build_trace_chunk_flushing, build.project) && !saved
|
||||
raise FailedToPersistDataError, 'Check PG logs'
|
||||
end
|
||||
|
||||
old_store_class.delete_data(self)
|
||||
end
|
||||
|
@ -4,6 +4,10 @@ module WorkItems
|
||||
class DatesSource < ApplicationRecord
|
||||
include FromUnion
|
||||
|
||||
# ElasticSearch is limited to use dates within this range
|
||||
MAX_DATE_LIMIT = Date.new(9999, 12, 31).freeze
|
||||
MIN_DATE_LIMIT = Date.new(1000, 1, 1).freeze
|
||||
|
||||
self.table_name = 'work_item_dates_sources'
|
||||
|
||||
# namespace is required as the sharding key
|
||||
@ -22,8 +26,29 @@ module WorkItems
|
||||
|
||||
scope :work_items_in, ->(work_items) { where(work_item: work_items) }
|
||||
|
||||
with_options(comparison: {
|
||||
allow_nil: true,
|
||||
less_than_or_equal_to: MAX_DATE_LIMIT,
|
||||
greater_than_or_equal_to: MIN_DATE_LIMIT
|
||||
}, if: :validate_dates?) do
|
||||
validates :start_date
|
||||
validates :start_date_fixed
|
||||
validates :due_date
|
||||
validates :due_date_fixed
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validate for new records or when any date field has changed
|
||||
def validate_dates?
|
||||
new_record? || any_dates_changed?
|
||||
end
|
||||
|
||||
def any_dates_changed?
|
||||
(changed_attributes.keys & %w[start_date start_date_fixed due_date due_date_fixed])
|
||||
.present?
|
||||
end
|
||||
|
||||
def set_namespace
|
||||
return if work_item.blank?
|
||||
return if work_item.namespace == namespace
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
name: self_heal_build_trace_chunk_flushing
|
||||
description:
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/555415
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199328
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/555523
|
||||
milestone: '18.3'
|
||||
group: group::pipeline execution
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
@ -22,6 +22,12 @@ sections_with_no_tw_review = {
|
||||
# One exception to the exceptions above: Technical Writing docs should get a TW review.
|
||||
TW_DOCS_PATH = 'doc/development/documentation'
|
||||
|
||||
# Some docs require a longer pipeline, which cannot be avoided.
|
||||
sections_with_tier3_review = {
|
||||
'doc/_index.md' => [],
|
||||
'doc/api/settings.md' => []
|
||||
}
|
||||
|
||||
docs_paths_to_review.reject! do |doc|
|
||||
section_with_no_tw_review = sections_with_no_tw_review.keys.find { |skip_path| doc.start_with?(skip_path) && !doc.start_with?(TW_DOCS_PATH) }
|
||||
next unless section_with_no_tw_review
|
||||
@ -31,6 +37,15 @@ docs_paths_to_review.reject! do |doc|
|
||||
true
|
||||
end
|
||||
|
||||
docs_paths_to_review.reject! do |doc|
|
||||
section = sections_with_tier3_review.keys.find { |skip_path| doc.start_with?(skip_path) && !doc.start_with?(TW_DOCS_PATH) }
|
||||
next unless section
|
||||
|
||||
sections_with_tier3_review[section] << doc
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
SOLUTIONS_LABELS = %w[Solutions].freeze
|
||||
DEVELOPMENT_LABELS = ['development guidelines'].freeze
|
||||
|
||||
@ -50,6 +65,10 @@ LOCALIZATION_MESSAGE = <<~MSG
|
||||
This MR contains files in the /doc-locale directory. These files are translations maintained through a separate process and should not be edited directly. If you are not part of the Localization team, please remove the changes to these files from your MR.
|
||||
MSG
|
||||
|
||||
DOCS_LONG_PIPELINE_MESSAGE = <<~MSG
|
||||
This merge request contains documentation files that require a tier-3 code pipeline before merge. After you complete all needed documentation reviews with short docs pipelines, see the [instructions for running a long pipeline](https://docs.gitlab.com/development/documentation/workflow/#pipelines-and-branch-naming) to this merge request.
|
||||
MSG
|
||||
|
||||
# For regular pages, prompt for a TW review
|
||||
DOCS_UPDATE_SHORT_MESSAGE = <<~MSG
|
||||
This merge request adds or changes documentation files and requires Technical Writing review. The review should happen before merge, but can be post-merge if the merge request is time sensitive.
|
||||
@ -98,6 +117,8 @@ end
|
||||
|
||||
message(LOCALIZATION_MESSAGE) if sections_with_no_tw_review["doc-locale"].any?
|
||||
|
||||
message(DOCS_LONG_PIPELINE_MESSAGE) if sections_with_tier3_review.values.flatten.any?
|
||||
|
||||
unless docs_paths_to_review.empty?
|
||||
message(DOCS_UPDATE_SHORT_MESSAGE)
|
||||
markdown(DOCS_UPDATE_LONG_MESSAGE)
|
||||
|
@ -1043,6 +1043,23 @@ This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, and `last: Int`.
|
||||
|
||||
### `Query.mavenVirtualRegistry`
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 18.3.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Find a Maven virtual registry. Returns null if the `maven_virtual_registry` feature flag is disabled.
|
||||
|
||||
Returns [`MavenVirtualRegistry`](#mavenvirtualregistry).
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="querymavenvirtualregistryid"></a>`id` | [`VirtualRegistriesPackagesMavenRegistryID!`](#virtualregistriespackagesmavenregistryid) | Global ID of the Maven virtual registry. |
|
||||
|
||||
### `Query.memberRole`
|
||||
|
||||
{{< details >}}
|
||||
|
@ -237,6 +237,8 @@ module API
|
||||
end
|
||||
|
||||
resource :job do
|
||||
helpers ::API::Helpers::KasHelpers
|
||||
|
||||
before do
|
||||
# Use primary for both main and ci database as authenticating in the scope of runners will load
|
||||
# Ci::Build model and other standard authn related models like License, Project and User.
|
||||
@ -294,6 +296,8 @@ module API
|
||||
pipeline.project
|
||||
).execute
|
||||
|
||||
set_feature_flag_header(user: current_user, project: project)
|
||||
|
||||
# See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api
|
||||
{
|
||||
allowed_agents: Entities::Clusters::Agents::Authorizations::CiAccess.represent(agent_authorizations),
|
||||
|
@ -3,6 +3,8 @@
|
||||
module API
|
||||
module Helpers
|
||||
module KasHelpers
|
||||
FEATURE_FLAG_HEADER_NAME = 'Gitlab-Feature-Flag'
|
||||
|
||||
def authenticate_gitlab_kas_request!
|
||||
render_api_error!('KAS JWT authentication invalid', 401) unless Gitlab::Kas.verify_api_request(headers)
|
||||
end
|
||||
@ -14,6 +16,26 @@ module API
|
||||
def gitaly_repository(project)
|
||||
project.repository.gitaly_repository.to_h
|
||||
end
|
||||
|
||||
def set_feature_flag_header(user: nil, project: nil, group: nil)
|
||||
# set feature flag headers
|
||||
feature_flags = ::Feature::Kas.server_feature_flags_for_http_response(
|
||||
user: ::Feature::Kas.user_actor(user),
|
||||
project: ::Feature::Kas.project_actor(project),
|
||||
group: ::Feature::Kas.group_actor(group)
|
||||
)
|
||||
header FEATURE_FLAG_HEADER_NAME, feature_flags.map { |k, v| "#{k}=#{v}" }.join(', ')
|
||||
end
|
||||
|
||||
def set_feature_flag_header_for_agent(user: nil, agent: nil)
|
||||
# set feature flag headers
|
||||
feature_flags = ::Feature::Kas.server_feature_flags_for_http_response(
|
||||
user: ::Feature::Kas.user_actor(user),
|
||||
project: ::Feature::Kas.project_actor(agent),
|
||||
group: ::Feature::Kas.group_actor(agent)
|
||||
)
|
||||
header FEATURE_FLAG_HEADER_NAME, feature_flags.map { |k, v| "#{k}=#{v}" }.join(', ')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -119,6 +119,8 @@ module API
|
||||
service_response = ::Clusters::Agents::AuthorizeProxyUserService.new(user, agent).execute
|
||||
render_api_error!(service_response[:message], service_response[:reason]) unless service_response.success?
|
||||
|
||||
set_feature_flag_header_for_agent(user: user, agent: agent)
|
||||
|
||||
service_response.payload
|
||||
end
|
||||
end
|
||||
|
87
lib/feature/kas.rb
Normal file
87
lib/feature/kas.rb
Normal file
@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Feature
|
||||
class Kas
|
||||
PREFIX = "kas_"
|
||||
|
||||
class << self
|
||||
def enabled_for_any?(feature_flag, *actors)
|
||||
return false unless Feature::FlipperFeature.table_exists?
|
||||
|
||||
actors = actors.compact
|
||||
# rubocop:disable Gitlab/FeatureFlagKeyDynamic -- this is a helper function and we need a variable argument here
|
||||
# rubocop:disable Gitlab/FeatureFlagWithoutActor -- we explicitly don't want an actor here
|
||||
return Feature.enabled?(feature_flag, type: :undefined, default_enabled_if_undefined: false) if actors.empty?
|
||||
|
||||
# rubocop:enable Gitlab/FeatureFlagWithoutActor
|
||||
|
||||
actors.any? do |actor|
|
||||
Feature.enabled?(feature_flag, actor, type: :undefined, default_enabled_if_undefined: false)
|
||||
end
|
||||
# rubocop:enable Gitlab/FeatureFlagKeyDynamic
|
||||
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
|
||||
false
|
||||
end
|
||||
|
||||
def server_feature_flags_for_grpc_request(user: nil, project: nil, group: nil)
|
||||
server_feature_flags(
|
||||
->(f) { "kas-feature-#{f.delete_prefix(PREFIX).tr('_', '-')}" },
|
||||
user: user,
|
||||
project: project,
|
||||
group: group
|
||||
)
|
||||
end
|
||||
|
||||
def server_feature_flags_for_http_response(user: nil, project: nil, group: nil)
|
||||
server_feature_flags(
|
||||
->(f) { f.delete_prefix(PREFIX) },
|
||||
user: user,
|
||||
project: project,
|
||||
group: group
|
||||
)
|
||||
end
|
||||
|
||||
def user_actor(user = nil)
|
||||
return ::User.actor_from_id(user.id) if user.is_a?(::User)
|
||||
|
||||
user_id = Gitlab::ApplicationContext.current_context_attribute(:user_id)
|
||||
::User.actor_from_id(user_id) if user_id
|
||||
end
|
||||
|
||||
def project_actor(container)
|
||||
return unless container
|
||||
|
||||
return ::Project.actor_from_id(container.project.id) if container.is_a?(::Clusters::Agent)
|
||||
|
||||
::Project.actor_from_id(container.id) if container.is_a?(::Project)
|
||||
end
|
||||
|
||||
def group_actor(container)
|
||||
return unless container
|
||||
|
||||
return ::Group.actor_from_id(container.id) if container.is_a?(::Group)
|
||||
return ::Group.actor_from_id(container.project.namespace_id) if container.is_a?(::Clusters::Agent)
|
||||
|
||||
::Group.actor_from_id(container.namespace_id) if container.is_a?(::Project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def server_feature_flags(key_transform, user: nil, project: nil, group: nil)
|
||||
# We need to check that both the DB connection and table exists
|
||||
return {} unless FlipperFeature.database.cached_table_exists?
|
||||
|
||||
# The order of actors here is significant.
|
||||
# Percentage-based actor selection may not work as expected if this order changes.
|
||||
actors = [user, project, group].compact
|
||||
|
||||
Feature.persisted_names
|
||||
.select { |f| f.start_with?(PREFIX) }
|
||||
.to_h do |f|
|
||||
key = key_transform ? key_transform.call(f) : f
|
||||
[key, enabled_for_any?(f, *actors).to_s]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -51,7 +51,10 @@ module Gitlab
|
||||
)
|
||||
|
||||
stub_for(:configuration_project)
|
||||
.list_agent_config_files(request, metadata: metadata)
|
||||
.list_agent_config_files(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(project),
|
||||
group: ::Feature::Kas.group_actor(project)
|
||||
))
|
||||
.config_files
|
||||
.to_a
|
||||
end
|
||||
@ -67,7 +70,10 @@ module Gitlab
|
||||
)
|
||||
|
||||
stub_for(:notifications)
|
||||
.git_push_event(request, metadata: metadata)
|
||||
.git_push_event(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(project),
|
||||
group: ::Feature::Kas.group_actor(project)
|
||||
))
|
||||
end
|
||||
|
||||
def send_autoflow_event(project:, type:, id:, data:)
|
||||
@ -99,7 +105,10 @@ module Gitlab
|
||||
)
|
||||
|
||||
stub_for(:autoflow)
|
||||
.cloud_event(request, metadata: metadata)
|
||||
.cloud_event(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(project),
|
||||
group: ::Feature::Kas.group_actor(project)
|
||||
))
|
||||
end
|
||||
|
||||
def get_environment_template(agent:, template_name:)
|
||||
@ -114,7 +123,10 @@ module Gitlab
|
||||
)
|
||||
|
||||
stub_for(:managed_resources)
|
||||
.get_environment_template(request, metadata: metadata)
|
||||
.get_environment_template(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(project),
|
||||
group: ::Feature::Kas.group_actor(project)
|
||||
))
|
||||
.template
|
||||
end
|
||||
|
||||
@ -132,7 +144,10 @@ module Gitlab
|
||||
data: template.data),
|
||||
info: templating_info(environment:, build:))
|
||||
stub_for(:managed_resources)
|
||||
.render_environment_template(request, metadata: metadata)
|
||||
.render_environment_template(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(environment.project),
|
||||
group: ::Feature::Kas.group_actor(environment.project)
|
||||
))
|
||||
.template
|
||||
end
|
||||
|
||||
@ -143,7 +158,10 @@ module Gitlab
|
||||
data: template.data),
|
||||
info: templating_info(environment:, build:))
|
||||
stub_for(:managed_resources)
|
||||
.ensure_environment(request, metadata: metadata)
|
||||
.ensure_environment(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(environment.project),
|
||||
group: ::Feature::Kas.group_actor(environment.project)
|
||||
))
|
||||
end
|
||||
|
||||
def delete_environment(managed_resource:)
|
||||
@ -154,7 +172,10 @@ module Gitlab
|
||||
objects: managed_resource.tracked_objects
|
||||
)
|
||||
|
||||
stub_for(:managed_resources).delete_environment(request, metadata: metadata)
|
||||
stub_for(:managed_resources).delete_environment(request, metadata: metadata(
|
||||
project: ::Feature::Kas.project_actor(managed_resource.project),
|
||||
group: ::Feature::Kas.group_actor(managed_resource.project)
|
||||
))
|
||||
end
|
||||
|
||||
private
|
||||
@ -190,8 +211,11 @@ module Gitlab
|
||||
end
|
||||
end
|
||||
|
||||
def metadata
|
||||
{ 'authorization' => "bearer #{token}" }
|
||||
def metadata(**feature_flag_actors)
|
||||
{
|
||||
'authorization' => "bearer #{token}",
|
||||
**::Feature::Kas.server_feature_flags_for_grpc_request(**feature_flag_actors)
|
||||
}
|
||||
end
|
||||
|
||||
def token
|
||||
|
@ -63,6 +63,8 @@ module RuboCop
|
||||
if flag_arg_is_str_or_sym?(flag_arg)
|
||||
if caller_is_feature_gitaly?(node)
|
||||
save_used_feature_flag("gitaly_#{flag_value}")
|
||||
elsif caller_is_feature_kas?(node)
|
||||
save_used_feature_flag("kas_#{flag_value}")
|
||||
else
|
||||
save_used_feature_flag(flag_value)
|
||||
end
|
||||
@ -158,8 +160,12 @@ module RuboCop
|
||||
class_caller(node) == "Feature::Gitaly"
|
||||
end
|
||||
|
||||
def caller_is_feature_kas?(node)
|
||||
class_caller(node) == "Feature::Kas"
|
||||
end
|
||||
|
||||
def feature_method?(node)
|
||||
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node))
|
||||
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node) || caller_is_feature_kas?(node))
|
||||
end
|
||||
|
||||
def worker_method?(node)
|
||||
|
@ -5,6 +5,29 @@ module RuboCop
|
||||
module Performance
|
||||
# Cop that disallows certain methods that rely on subtransactions in their implementation.
|
||||
# Companion to Performance/ActiveRecordSubtransactions, which bans direct usage of subtransactions.
|
||||
# @example
|
||||
#
|
||||
# # bad
|
||||
# User.create_or_find_by(name: 'Alice')
|
||||
#
|
||||
# # good
|
||||
# User.find_or_create_by(name: 'Alice')
|
||||
#
|
||||
# # Disallowed method `safe_find_or_create_by` or `safe_find_or_create_by!`
|
||||
# # bad
|
||||
# User.safe_find_or_create_by!(email: 'alice@example.com')
|
||||
#
|
||||
# # good
|
||||
# User.find_or_create_by!(email: 'alice@example.com')
|
||||
#
|
||||
# # Disallowed method `with_fast_read_statement_timeout`
|
||||
# # bad
|
||||
# record.with_fast_read_statement_timeout do
|
||||
# record.some_heavy_read_operation
|
||||
# end
|
||||
#
|
||||
# # good
|
||||
# record.some_heavy_read_operation
|
||||
class ActiveRecordSubtransactionMethods < RuboCop::Cop::Base
|
||||
MSG = 'Methods that rely on subtransactions should not be used. ' \
|
||||
'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346'
|
||||
|
@ -3,6 +3,17 @@
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Performance
|
||||
# Cop that bans direct usage of subtransactions in active record.
|
||||
# @example
|
||||
# # bad
|
||||
# ActiveRecord::Base.transaction(requires_new: true) do
|
||||
# user.update!(active: true)
|
||||
# end
|
||||
#
|
||||
# # good
|
||||
# ActiveRecord::Base.transaction do
|
||||
# user.update!(active: true)
|
||||
# end
|
||||
class ActiveRecordSubtransactions < RuboCop::Cop::Base
|
||||
MSG = 'Subtransactions should not be used. ' \
|
||||
'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346'
|
||||
|
@ -3,6 +3,15 @@
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Performance
|
||||
# Cop that encourages efficient use of counts on collection by substituting more than one queries with one query.
|
||||
# @example
|
||||
# # bad
|
||||
# users.count
|
||||
# users.each { |u| work(u) }
|
||||
#
|
||||
# # good
|
||||
# users.load.size
|
||||
# users.each { |u| work(u) }
|
||||
class ARCountEach < RuboCop::Cop::Base
|
||||
def message(ivar)
|
||||
"If #{ivar} is AR relation, avoid `#{ivar}.count ...; #{ivar}.each... `, this will trigger two queries. " \
|
||||
|
@ -3,6 +3,14 @@
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Performance
|
||||
# Cop that detects inefficient patterns using ActiveRecord's `exists?`, `present?`,
|
||||
# or `blank?` in a suboptimal way.
|
||||
# @example
|
||||
# # bad
|
||||
# users.present?
|
||||
#
|
||||
# # good
|
||||
# users.any?
|
||||
class ARExistsAndPresentBlank < RuboCop::Cop::Base
|
||||
def message_present(ivar)
|
||||
"Avoid `#{ivar}.present?`, because it will generate database query 'Select TABLE.*' which is expensive. "\
|
||||
|
@ -3,6 +3,17 @@
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Performance
|
||||
# Cop that encourages streaming file reading instead of reading the entire file into memory.
|
||||
# @example
|
||||
# # bad
|
||||
# File.readlines('some_file.txt').each do |line|
|
||||
# process(line)
|
||||
# end
|
||||
#
|
||||
# # good
|
||||
# File.each_line('some_file.txt') do |line|
|
||||
# process(line)
|
||||
# end
|
||||
class ReadlinesEach < RuboCop::Cop::Base
|
||||
extend RuboCop::Cop::AutoCorrector
|
||||
|
||||
|
363
spec/lib/feature/kas_spec.rb
Normal file
363
spec/lib/feature/kas_spec.rb
Normal file
@ -0,0 +1,363 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Feature::Kas, feature_category: :deployment_management do
|
||||
let_it_be(:cluster) { create(:cluster_agent) }
|
||||
let_it_be(:cluster_2) { create(:cluster_agent) }
|
||||
|
||||
let_it_be(:project) { cluster.project }
|
||||
let_it_be(:project_2) { cluster_2.project }
|
||||
|
||||
before do
|
||||
allow(Feature::Definition).to receive(:get).and_call_original
|
||||
allow(Feature::Definition).to receive(:get).with(:flag).and_return(
|
||||
Feature::Definition.new('flag.yml', name: :flag, type: :undefined)
|
||||
)
|
||||
end
|
||||
|
||||
describe ".enabled_for_any?" do
|
||||
context 'when the flag is set globally' do
|
||||
context 'when the gate is closed' do
|
||||
before do
|
||||
stub_feature_flags(kas_global_flag: false)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(described_class.enabled_for_any?(:kas_global_flag)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the flag defaults to on' do
|
||||
it 'returns true' do
|
||||
expect(described_class.enabled_for_any?(:kas_global_flag)).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the flag is enabled for a particular project' do
|
||||
before do
|
||||
stub_feature_flags(kas_project_flag: project)
|
||||
end
|
||||
|
||||
it 'returns true for that project' do
|
||||
expect(described_class.enabled_for_any?(:kas_project_flag, project)).to be(true)
|
||||
end
|
||||
|
||||
it 'returns false for any other project' do
|
||||
expect(described_class.enabled_for_any?(:kas_project_flag, project_2)).to be(false)
|
||||
end
|
||||
|
||||
it 'returns false when no project is passed' do
|
||||
expect(described_class.enabled_for_any?(:kas_project_flag)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the flag is checked with multiple input actors' do
|
||||
before do
|
||||
stub_feature_flags(kas_flag: project)
|
||||
end
|
||||
|
||||
it 'returns true if any of the flag is enabled for any of the input actors' do
|
||||
expect(described_class.enabled_for_any?(:kas_flag, project, project.group)).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".server_feature_flags_for_grpc_request" do
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(
|
||||
kas_global_flag: true,
|
||||
kas_project_flag: project,
|
||||
kas_user_flag: user,
|
||||
kas_group_flag: group,
|
||||
non_kas_flag: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns a hash of flags starting with the prefix, with dashes instead of underscores' do
|
||||
expect(described_class.server_feature_flags_for_grpc_request).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
end
|
||||
|
||||
context 'when a project is passed' do
|
||||
it 'returns the value for the flag on the given project' do
|
||||
expect(described_class.server_feature_flags_for_grpc_request(project: project)).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'true',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_grpc_request(project: project_2)).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user is passed' do
|
||||
it 'returns the value for the flag on the given user' do
|
||||
expect(described_class.server_feature_flags_for_grpc_request(user: user)).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'true',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_grpc_request(user: create(:user))).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a group is passed' do
|
||||
it 'returns the value for the flag on the given group' do
|
||||
expect(described_class.server_feature_flags_for_grpc_request(group: group)).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'true'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_grpc_request(group: create(:group))).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multiple actors are passed' do
|
||||
it 'returns the corresponding enablement status for actors' do
|
||||
expect(described_class.server_feature_flags_for_grpc_request(project: project_2, group: group)).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'false',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'true'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_grpc_request(project: project, group: project_2.group)).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'true',
|
||||
'kas-feature-user-flag' => 'false',
|
||||
'kas-feature-group-flag' => 'false'
|
||||
)
|
||||
|
||||
expect(
|
||||
described_class.server_feature_flags_for_grpc_request(user: user, project: project, group: group)
|
||||
).to eq(
|
||||
'kas-feature-global-flag' => 'true',
|
||||
'kas-feature-project-flag' => 'true',
|
||||
'kas-feature-user-flag' => 'true',
|
||||
'kas-feature-group-flag' => 'true'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when table does not exist' do
|
||||
before do
|
||||
allow(Feature::FlipperFeature.database)
|
||||
.to receive(:cached_table_exists?)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
it 'returns an empty Hash' do
|
||||
expect(described_class.server_feature_flags_for_grpc_request).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".server_feature_flags_for_http_response" do
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(
|
||||
kas_global_flag: true,
|
||||
kas_project_flag: project,
|
||||
kas_user_flag: user,
|
||||
kas_group_flag: group,
|
||||
non_kas_flag: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns a hash of flags' do
|
||||
expect(described_class.server_feature_flags_for_http_response).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
end
|
||||
|
||||
context 'when a project is passed' do
|
||||
it 'returns the value for the flag on the given project' do
|
||||
expect(described_class.server_feature_flags_for_http_response(project: project)).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'true',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_http_response(project: project_2)).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user is passed' do
|
||||
it 'returns the value for the flag on the given user' do
|
||||
expect(described_class.server_feature_flags_for_http_response(user: user)).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'true',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_http_response(user: create(:user))).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a group is passed' do
|
||||
it 'returns the value for the flag on the given group' do
|
||||
expect(described_class.server_feature_flags_for_http_response(group: group)).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'true'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_http_response(group: create(:group))).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multiple actors are passed' do
|
||||
it 'returns the corresponding enablement status for actors' do
|
||||
expect(described_class.server_feature_flags_for_http_response(project: project_2, group: group)).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'false',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'true'
|
||||
)
|
||||
|
||||
expect(described_class.server_feature_flags_for_http_response(project: project, group: project_2.group)).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'true',
|
||||
'user_flag' => 'false',
|
||||
'group_flag' => 'false'
|
||||
)
|
||||
|
||||
expect(
|
||||
described_class.server_feature_flags_for_http_response(user: user, project: project, group: group)
|
||||
).to eq(
|
||||
'global_flag' => 'true',
|
||||
'project_flag' => 'true',
|
||||
'user_flag' => 'true',
|
||||
'group_flag' => 'true'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when table does not exist' do
|
||||
before do
|
||||
allow(Feature::FlipperFeature.database)
|
||||
.to receive(:cached_table_exists?)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
it 'returns an empty Hash' do
|
||||
expect(described_class.server_feature_flags_for_http_response).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".user_actor" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when user is passed in' do
|
||||
it 'returns a actor wrapper from user' do
|
||||
expect(described_class.user_actor(user).flipper_id).to eql(user.flipper_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called without user and user_id is available in application context' do
|
||||
it 'returns a actor wrapper from user_id' do
|
||||
::Gitlab::ApplicationContext.with_context(user: user) do
|
||||
expect(described_class.user_actor.flipper_id).to eql(user.flipper_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called without user and user_id is absent from application context' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.user_actor).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when something else is passed' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.user_actor(1234)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".project_actor" do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
context 'when project is passed in' do
|
||||
it 'returns a actor wrapper from project' do
|
||||
expect(described_class.project_actor(project).flipper_id).to eql(project.flipper_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when something else is passed in' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.project_actor(1234)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".group_actor" do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
context 'when project is passed in' do
|
||||
it "returns a actor wrapper from project's group" do
|
||||
expect(described_class.group_actor(project).flipper_id).to eql(group.flipper_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when something else is passed in' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.group_actor(1234)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -34,6 +34,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
describe 'gRPC calls' do
|
||||
let(:token) { instance_double(JSONWebToken::HMACToken, encoded: 'test-token') }
|
||||
let(:kas_url) { 'grpc://example.kas.internal' }
|
||||
let(:feature_flags) { { 'kas-feature-a' => 'true', 'kas-feature-b': 'false' } }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
|
||||
@ -45,6 +46,8 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
|
||||
allow(token).to receive(:issuer=).with(Settings.gitlab.host)
|
||||
allow(token).to receive(:audience=).with(described_class::JWT_AUDIENCE)
|
||||
|
||||
allow(::Feature::Kas).to receive(:server_feature_flags_for_grpc_request).and_return(feature_flags)
|
||||
end
|
||||
|
||||
describe '#get_server_info' do
|
||||
@ -64,7 +67,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:get_server_info)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -90,7 +93,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:get_connected_agentks_by_agent_i_ds)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -129,7 +132,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:list_agent_config_files)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -191,7 +194,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:cloud_event)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -226,7 +229,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:git_push_event)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -278,7 +281,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:get_environment_template)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -302,7 +305,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:get_default_environment_template)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -340,7 +343,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:render_environment_template)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -378,7 +381,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
.and_return(request)
|
||||
|
||||
expect(stub).to receive(:ensure_environment)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
@ -408,7 +411,7 @@ RSpec.describe Gitlab::Kas::Client, feature_category: :deployment_management do
|
||||
).and_return(request)
|
||||
|
||||
expect(stub).to receive(:delete_environment)
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token' })
|
||||
.with(request, metadata: { 'authorization' => 'bearer test-token', **feature_flags })
|
||||
.and_return(response)
|
||||
end
|
||||
|
||||
|
@ -782,6 +782,136 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with self_heal_build_trace_chunk_flushing feature flag' do
|
||||
where(:data_store, :redis_class) do
|
||||
[
|
||||
[:redis, Ci::BuildTraceChunks::Redis],
|
||||
[:redis_trace_chunks, Ci::BuildTraceChunks::RedisTraceChunks]
|
||||
]
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:data) { 'a' * described_class::CHUNK_SIZE }
|
||||
|
||||
before do
|
||||
build_trace_chunk.send(:unsafe_set_data!, data)
|
||||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(self_heal_build_trace_chunk_flushing: true)
|
||||
end
|
||||
|
||||
context 'when save operation succeeds' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive_messages(changed?: true, save!: true)
|
||||
end
|
||||
|
||||
it 'deletes old data from redis store' do
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
subject
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when save operation fails' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive_messages(changed?: true, save!: false)
|
||||
end
|
||||
|
||||
it 'preserves old data to allow self-healing' do
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when save operation returns nil' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive_messages(changed?: true, save!: nil)
|
||||
end
|
||||
|
||||
it 'preserves old data to allow self-healing' do
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when save is not called due to no changes' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive(:changed?).and_return(false)
|
||||
end
|
||||
|
||||
it 'preserves old data since save was not attempted' do
|
||||
allow(build_trace_chunk).to receive(:changed?).and_return(false)
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when save raises an exception' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive(:changed?).and_return(true)
|
||||
allow(build_trace_chunk).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
|
||||
it 'preserves old data and re-raises exception' do
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(self_heal_build_trace_chunk_flushing: false)
|
||||
end
|
||||
|
||||
context 'when data is not saved' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive(:unsafe_set_data!).and_return(nil)
|
||||
end
|
||||
|
||||
it 'deletes old data' do
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
subject
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data is saved' do
|
||||
before do
|
||||
allow(build_trace_chunk).to receive(:unsafe_set_data!).and_return(true)
|
||||
end
|
||||
|
||||
it 'deletes old data' do
|
||||
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
|
||||
|
||||
subject
|
||||
|
||||
expect(redis_class.new.data(build_trace_chunk)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'final?' do
|
||||
|
@ -3,6 +3,8 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management do
|
||||
let_it_be(:work_item) { create(:work_item) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:namespace).inverse_of(:work_items_dates_source) }
|
||||
it { is_expected.to belong_to(:work_item).with_foreign_key('issue_id').inverse_of(:dates_source) }
|
||||
@ -13,7 +15,6 @@ RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management d
|
||||
end
|
||||
|
||||
it 'ensures to use work_item namespace' do
|
||||
work_item = create(:work_item)
|
||||
date_source = described_class.new(work_item: work_item)
|
||||
|
||||
expect(date_source).to be_valid
|
||||
@ -22,8 +23,6 @@ RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management d
|
||||
end
|
||||
|
||||
describe 'before_save' do
|
||||
let_it_be(:work_item) { create(:work_item) }
|
||||
|
||||
describe 'set_fixed_start_date' do
|
||||
context 'when start date is fixed' do
|
||||
it 'sets start_date to match fixed_start_date' do
|
||||
@ -63,6 +62,44 @@ RSpec.describe WorkItems::DatesSource, feature_category: :portfolio_management d
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
%i[start_date start_date_fixed due_date due_date_fixed].each do |field|
|
||||
context 'for new records' do
|
||||
it "validates #{field} minimum value" do
|
||||
dates_source = build(:work_items_dates_source, field => WorkItems::DatesSource::MIN_DATE_LIMIT - 1.day)
|
||||
|
||||
expect(dates_source).not_to be_valid
|
||||
expect(dates_source.errors[field]).to include('must be greater than or equal to 1000-01-01')
|
||||
end
|
||||
|
||||
it "validates #{field} maximum value" do
|
||||
dates_source = build(:work_items_dates_source, field => WorkItems::DatesSource::MAX_DATE_LIMIT + 1.day)
|
||||
|
||||
expect(dates_source).not_to be_valid
|
||||
expect(dates_source.errors[field]).to include('must be less than or equal to 9999-12-31')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for existing records' do
|
||||
it "validates #{field} only if it was updated", :aggregate_failures do
|
||||
dates_source = build(
|
||||
:work_items_dates_source,
|
||||
work_item: work_item,
|
||||
namespace: work_item.namespace,
|
||||
field => WorkItems::DatesSource::MAX_DATE_LIMIT + 1.day
|
||||
)
|
||||
dates_source.save!(validate: false)
|
||||
|
||||
expect(dates_source).to be_valid
|
||||
|
||||
dates_source[field] += 1.day
|
||||
expect(dates_source).not_to be_valid
|
||||
expect(dates_source.errors[field]).to include('must be less than or equal to 9999-12-31')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on database triggers' do
|
||||
let_it_be_with_reload(:work_item) { create(:work_item) }
|
||||
|
||||
|
@ -283,6 +283,10 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
|
||||
end
|
||||
|
||||
before do |example|
|
||||
allow(::Feature::Kas).to receive(:server_feature_flags_for_http_response).and_return(
|
||||
{ 'feature_flag_a' => 'true', 'feature_flag_b' => 'false' }
|
||||
)
|
||||
|
||||
unless example.metadata[:skip_before_request]
|
||||
subject
|
||||
end
|
||||
@ -314,6 +318,10 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
|
||||
|
||||
expect(json_response['allowed_agents']).to match_array expected_allowed_agents
|
||||
end
|
||||
|
||||
it 'returns feature flags in response header' do
|
||||
expect(response.headers.to_h).to include({ 'Gitlab-Feature-Flag' => 'feature_flag_a=true, feature_flag_b=false' })
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'valid allowed_agents request for a job with environment' do
|
||||
@ -331,6 +339,10 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
|
||||
|
||||
expect(json_response['allowed_agents']).to match_array(expected_allowed_agents)
|
||||
end
|
||||
|
||||
it 'returns feature flags in response header' do
|
||||
expect(response.headers.to_h).to include({ 'Gitlab-Feature-Flag' => 'feature_flag_a=true, feature_flag_b=false' })
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'valid allowed_agents request'
|
||||
|
@ -832,5 +832,23 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with feature flags' do
|
||||
before do
|
||||
allow(::Feature::Kas).to receive(:server_feature_flags_for_http_response).and_return(
|
||||
{ 'feature_flag_a' => 'true', 'feature_flag_b' => 'false' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns feature flags in response header' do
|
||||
deployment_project.add_member(user, :developer)
|
||||
token = new_token
|
||||
public_id = stub_user_session(user, token)
|
||||
access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
|
||||
send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
|
||||
|
||||
expect(response.headers.to_h).to include({ 'Gitlab-Feature-Flag' => 'feature_flag_a=true, feature_flag_b=false' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -119,6 +119,41 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
|
||||
end
|
||||
end
|
||||
|
||||
%w[
|
||||
Feature::Kas.enabled?
|
||||
Feature::Kas.disabled?
|
||||
].each do |feature_flag_method|
|
||||
context "#{feature_flag_method} method", feature_category: :deployment_management do
|
||||
context 'a string feature flag' do
|
||||
include_examples 'sets flag as used', %|#{feature_flag_method}("foo")|, 'kas_foo'
|
||||
end
|
||||
|
||||
context 'a symbol feature flag' do
|
||||
include_examples 'sets flag as used', %|#{feature_flag_method}(:foo)|, 'kas_foo'
|
||||
end
|
||||
|
||||
context 'an interpolated string feature flag with a string prefix' do
|
||||
include_examples 'sets flag as used', %|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
|
||||
end
|
||||
|
||||
context 'an interpolated symbol feature flag with a string prefix' do
|
||||
include_examples 'sets flag as used', %|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
|
||||
end
|
||||
|
||||
context 'an interpolated string feature flag with a string prefix and suffix' do
|
||||
include_examples 'does not set any flags as used', %|#{feature_flag_method}(:"foo_\#{bar}_baz")|
|
||||
end
|
||||
|
||||
context 'a dynamic string feature flag as a variable' do
|
||||
include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
|
||||
end
|
||||
|
||||
context 'an integer feature flag' do
|
||||
include_examples 'does not set any flags as used', %|#{feature_flag_method}(123)|
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the experiment method' do
|
||||
context 'a string feature flag' do
|
||||
include_examples 'sets flag as used', %q|experiment("baz")|, %w[baz]
|
||||
|
@ -27,5 +27,43 @@ RSpec.describe Ci::BuildTraceChunkFlushWorker, feature_category: :continuous_int
|
||||
expect(chunk.reload).to be_migrated
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop: disable RSpec/AnyInstanceOf -- next_instance_of will not work here
|
||||
context 'when save operation fails and self-healing is enabled' do
|
||||
before do
|
||||
stub_feature_flags(self_heal_build_trace_chunk_flushing: true)
|
||||
end
|
||||
|
||||
it 'preserves Redis data on first failure and completes migration on retry' do
|
||||
expect(chunk).to be_live
|
||||
|
||||
allow_any_instance_of(Ci::BuildTraceChunk).to receive(:save!).and_return(false)
|
||||
|
||||
# First run save! fails so we should still have redis data
|
||||
expect do
|
||||
described_class.new.perform(chunk.id)
|
||||
end.to raise_error(Ci::BuildTraceChunk::FailedToPersistDataError)
|
||||
|
||||
chunk.reload
|
||||
expect(chunk).to be_live
|
||||
expect(chunk).not_to be_migrated
|
||||
|
||||
redis_data = Ci::BuildTraceChunks::RedisTraceChunks.new.data(chunk)
|
||||
expect(redis_data).to eq(data)
|
||||
|
||||
allow_any_instance_of(Ci::BuildTraceChunk).to receive(:save!).and_call_original
|
||||
|
||||
# Second run it recovers
|
||||
described_class.new.perform(chunk.id)
|
||||
|
||||
chunk.reload
|
||||
expect(chunk).to be_migrated
|
||||
expect(chunk).not_to be_live
|
||||
|
||||
redis_data = Ci::BuildTraceChunks::RedisTraceChunks.new.data(chunk)
|
||||
expect(redis_data).to be_nil
|
||||
end
|
||||
end
|
||||
# rubocop: enable RSpec/AnyInstanceOf
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user