diff options
-rw-r--r-- | app/graphql/types/environment_type.rb | 6 | ||||
-rw-r--r-- | app/graphql/types/permission_types/environment.rb | 11 | ||||
-rw-r--r-- | app/services/merge_requests/create_service.rb | 2 | ||||
-rw-r--r-- | doc/api/graphql/reference/index.md | 11 | ||||
-rw-r--r-- | lib/gitlab/graphql/expose_permissions.rb | 8 | ||||
-rw-r--r-- | qa/qa/resource/bulk_import_group.rb | 2 | ||||
-rw-r--r-- | qa/qa/resource/issuable.rb | 3 | ||||
-rw-r--r-- | qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb | 93 | ||||
-rw-r--r-- | qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb | 4 | ||||
-rw-r--r-- | scripts/lib/glfm/update_specification.rb | 2 | ||||
-rw-r--r-- | spec/graphql/types/environment_type_spec.rb | 1 | ||||
-rw-r--r-- | spec/graphql/types/permission_types/environment_spec.rb | 15 | ||||
-rw-r--r-- | spec/requests/api/graphql/project/environments_spec.rb | 54 |
13 files changed, 166 insertions, 46 deletions
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index dd2286d333d..2249fa49f06 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -9,6 +9,12 @@ module Types authorize :read_environment + expose_permissions Types::PermissionTypes::Environment, + description: 'Permissions for the current user on the resource. '\ + 'This field can only be resolved for one environment in any single request.' do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + field :name, GraphQL::Types::String, null: false, description: 'Human-readable name of the environment.' diff --git a/app/graphql/types/permission_types/environment.rb b/app/graphql/types/permission_types/environment.rb new file mode 100644 index 00000000000..59c9fce64e5 --- /dev/null +++ b/app/graphql/types/permission_types/environment.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Environment < BasePermissionType + graphql_name 'EnvironmentPermissions' + + abilities :update_environment, :destroy_environment, :stop_environment + end + end +end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 04d08f257f1..8fa80dc3513 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -39,7 +39,7 @@ module MergeRequests # open while the Gitaly RPC waits. To avoid an idle in transaction # timeout, we do this before we attempt to save the merge request. - if Feature.enabled?(:async_merge_request_diff_creation, current_user) + if Feature.enabled?(:async_merge_request_diff_creation, merge_request.target_project) merge_request.skip_ensure_merge_request_diff = true else merge_request.eager_fetch_ref! diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 79202e500b7..e1fe6a0c1bf 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -12375,6 +12375,7 @@ Describes where code is deployed for a project. | <a id="environmentstate"></a>`state` | [`String!`](#string) | State of the environment, for example: available/stopped. | | <a id="environmenttier"></a>`tier` | [`DeploymentTier`](#deploymenttier) | Deployment tier of the environment. | | <a id="environmentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the environment was updated. | +| <a id="environmentuserpermissions"></a>`userPermissions` | [`EnvironmentPermissions!`](#environmentpermissions) | Permissions for the current user on the resource. This field can only be resolved for one environment in any single request. | #### Fields with arguments @@ -12419,6 +12420,16 @@ Returns [`MetricsDashboard`](#metricsdashboard). | ---- | ---- | ----------- | | <a id="environmentmetricsdashboardpath"></a>`path` | [`String!`](#string) | Path to a file which defines a metrics dashboard eg: `"config/prometheus/common_metrics.yml"`. | +### `EnvironmentPermissions` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="environmentpermissionsdestroyenvironment"></a>`destroyEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_environment` on this resource. | +| <a id="environmentpermissionsstopenvironment"></a>`stopEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `stop_environment` on this resource. | +| <a id="environmentpermissionsupdateenvironment"></a>`updateEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `update_environment` on this resource. | + ### `Epic` Represents an epic. diff --git a/lib/gitlab/graphql/expose_permissions.rb b/lib/gitlab/graphql/expose_permissions.rb index ab9ed354673..070d3c188a4 100644 --- a/lib/gitlab/graphql/expose_permissions.rb +++ b/lib/gitlab/graphql/expose_permissions.rb @@ -5,11 +5,15 @@ module Gitlab module ExposePermissions extend ActiveSupport::Concern prepended do - def self.expose_permissions(permission_type, description: 'Permissions for the current user on the resource') + def self.expose_permissions( + permission_type, + description: 'Permissions for the current user on the resource', + &block) field :user_permissions, permission_type, description: description, null: false, - method: :itself + method: :itself, + &block end end end diff --git a/qa/qa/resource/bulk_import_group.rb b/qa/qa/resource/bulk_import_group.rb index 2daf9684bbc..19ad5f1faf2 100644 --- a/qa/qa/resource/bulk_import_group.rb +++ b/qa/qa/resource/bulk_import_group.rb @@ -93,7 +93,7 @@ module QA # override transformation only for /bulk_imports endpoint which doesn't have web_url in response and # ignore others so import_id is not overwritten incorrectly - api_resource[:web_url] = "#{source_gitlab_address}/#{full_path}" + api_resource[:web_url] = "#{QA::Runtime::Scenario.gitlab_address}/#{full_path}" api_resource[:import_id] = api_resource[:id] api_resource end diff --git a/qa/qa/resource/issuable.rb b/qa/qa/resource/issuable.rb index 6ebdaac8298..5aee27b46d4 100644 --- a/qa/qa/resource/issuable.rb +++ b/qa/qa/resource/issuable.rb @@ -3,6 +3,8 @@ module QA module Resource class Issuable < Base + using Rainbow + # Commentes (notes) path # # @return [String] @@ -14,6 +16,7 @@ module QA # # @return [Array] def comments(auto_paginate: false, attempts: 0) + Runtime::Logger.debug("Fetching comments for #{self.class.name.black.bg(:white)} with path '#{api_get_path}'") return parse_body(api_get_from(api_comments_path)) unless auto_paginate auto_paginated_response( diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index 985edf3efc1..a7fe9c7630e 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -13,13 +13,7 @@ module QA let!(:source_gitlab_address) { ENV["QA_LARGE_IMPORT_SOURCE_URL"] || "https://gitlab.com" } let!(:gitlab_source_group) { ENV["QA_LARGE_IMPORT_GROUP"] || "gitlab-migration-large-import-test" } let!(:gitlab_source_project) { ENV["QA_LARGE_IMPORT_REPO"] || "migration-test-project" } - - let!(:import_wait_duration) do - { - max_duration: (ENV["QA_LARGE_IMPORT_DURATION"] || 3600).to_i, - sleep_interval: 30 - } - end + let!(:import_wait_duration) { { max_duration: (ENV["QA_LARGE_IMPORT_DURATION"] || 3600).to_i, sleep_interval: 30 } } let!(:source_admin_user) { "no-op" } let!(:source_admin_api_client) do @@ -56,8 +50,8 @@ module QA let(:source_labels) { source_project.labels(auto_paginate: true).map { |l| l.except(:id) } } let(:source_milestones) { source_project.milestones(auto_paginate: true).map { |ms| ms.except(:id, :web_url, :project_id) } } let(:source_pipelines) { source_project.pipelines(auto_paginate: true).map { |pp| pp.except(:id, :web_url, :project_id) } } - let(:source_mrs) { fetch_mrs(source_project, source_api_client) } - let(:source_issues) { fetch_issues(source_project, source_api_client) } + let(:source_mrs) { fetch_mrs(source_project, source_api_client, transform_urls: true) } + let(:source_issues) { fetch_issues(source_project, source_api_client, transform_urls: true) } # Imported objects # @@ -66,12 +60,10 @@ module QA let(:commits) { imported_project.commits(auto_paginate: true).map { |c| c[:id] } } let(:labels) { imported_project.labels(auto_paginate: true).map { |l| l.except(:id) } } let(:milestones) { imported_project.milestones(auto_paginate: true).map { |ms| ms.except(:id, :web_url, :project_id) } } - let(:pipelines) { imported_project.pipelines.map { |pp| pp.except(:id, :web_url, :project_id) } } + let(:pipelines) { imported_project.pipelines(auto_paginate: true).map { |pp| pp.except(:id, :web_url, :project_id) } } let(:mrs) { fetch_mrs(imported_project, api_client) } let(:issues) { fetch_issues(imported_project, api_client) } - let(:import_failures) { imported_group.import_details.sum([]) { |details| details[:failures] } } - before do Runtime::Feature.enable(:bulk_import_projects) unless Runtime::Feature.enabled?(:bulk_import_projects) end @@ -139,10 +131,7 @@ module QA # wait for import to finish and save import time logger.info("== Waiting for import to be finished ==") - expect { imported_group.import_status }.not_to eventually_eq("started").within(import_wait_duration) - # finished status actually means success, don't wait for finished status explicitly - # because test would wait full duration if returned status is "failed" - expect(imported_group.import_status).to eq("finished") + expect_group_import_finished_successfully @import_time = Time.now - start @@ -305,11 +294,12 @@ module QA # # @param [QA::Resource::Project] # @param [Runtime::API::Client] client + # @param [Boolean] transform_urls # @return [Hash] - def fetch_mrs(project, client) + def fetch_mrs(project, client, transform_urls: false) imported_mrs = project.merge_requests(auto_paginate: true, attempts: 2) - Parallel.map(imported_mrs, in_threads: 4) do |mr| + Parallel.map(imported_mrs, in_threads: 6) do |mr| resource = Resource::MergeRequest.init do |resource| resource.project = project resource.iid = mr[:iid] @@ -319,11 +309,11 @@ module QA [mr[:iid], { url: mr[:web_url], title: mr[:title], - body: sanitize_description(mr[:description]) || '', + body: sanitize_description(mr[:description], transform_urls) || '', state: mr[:state], comments: resource .comments(auto_paginate: true, attempts: 2) - .map { |c| sanitize_comment(c[:body]) } + .map { |c| sanitize_comment(c[:body], transform_urls) } }] end.to_h end @@ -332,11 +322,12 @@ module QA # # @param [QA::Resource::Project] # @param [Runtime::API::Client] client + # @param [Boolean] transform_urls # @return [Hash] - def fetch_issues(project, client) + def fetch_issues(project, client, transform_urls: false) imported_issues = project.issues(auto_paginate: true, attempts: 2) - Parallel.map(imported_issues, in_threads: 4) do |issue| + Parallel.map(imported_issues, in_threads: 6) do |issue| resource = Resource::Issue.init do |issue_resource| issue_resource.project = project issue_resource.iid = issue[:iid] @@ -347,42 +338,66 @@ module QA url: issue[:web_url], title: issue[:title], state: issue[:state], - body: sanitize_description(issue[:description]) || '', + body: sanitize_description(issue[:description], transform_urls) || '', comments: resource .comments(auto_paginate: true, attempts: 2) - .map { |c| sanitize_comment(c[:body]) } + .map { |c| sanitize_comment(c[:body], transform_urls) } }] end.to_h end - # Importer user mention pattern + # Remove added postfixes and transform urls # - # @return [Regex] - def created_by_pattern - @created_by_pattern ||= /\n\n \*By #{importer_username_pattern} on \S+ \(imported from GitLab\)\*/ + # Source urls need to be replaced with target urls for comparison to work + # + # @param [String] body + # @param [Boolean] transform_urls + # @return [String] + def sanitize_comment(body, transform_urls) + comment = body&.gsub(created_by_pattern, "") + return comment unless transform_urls + + comment&.gsub(source_project_url, imported_project_url) end - # Username of importer user for removal from comments and descriptions + # Remove added postfixes and transform urls + # + # Source urls need to be replaced with target urls for comparison to work # + # @param [String] body + # @param [Boolean] transform_urls # @return [String] - def importer_username_pattern - @importer_username_pattern ||= ENV['QA_LARGE_IMPORT_USER_PATTERN'] || "(gitlab-migration|GitLab QA Bot)" + def sanitize_description(body, transform_urls) + description = body&.gsub(created_by_pattern, "") + return description unless transform_urls + + description&.gsub(source_project_url, imported_project_url) end - # Remove added prefixes from comments + # Following objects are memoized via instance variables due to Parallel having some type of issue calling + # helpers defined via rspec let method + + # Importer user mention pattern + # + # @return [Regex] + def created_by_pattern + @created_by_pattern ||= /\n\n \*By .+ on \S+ \(imported from GitLab\)\*/ + end + + # Source project url # - # @param [String] body # @return [String] - def sanitize_comment(body) - body&.gsub(created_by_pattern, "") + def source_project_url + @source_group_url ||= "#{source_gitlab_address}/#{source_project.full_path}" end - # Remove created by prefix from descripion + # Imported project url + # + # This needs to be constructed manually because it is called before project import finishes # - # @param [String] body # @return [String] - def sanitize_description(body) - body&.gsub(created_by_pattern, "") + def imported_project_url + @imported_group_url ||= "#{Runtime::Scenario.gitlab_address}/#{imported_group.full_path}/#{source_project.path}" end # Save json as file diff --git a/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb b/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb index f9df6e805c0..6075efd348b 100644 --- a/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb +++ b/qa/qa/specs/features/shared_contexts/import/gitlab_group_migration_common.rb @@ -79,13 +79,13 @@ module QA imported_group # trigger import status = nil - Support::Retrier.retry_until(**import_wait_duration, message: "Import did not complete") do + Support::Retrier.retry_until(**import_wait_duration, raise_on_failure: false) do status = imported_group.import_status %w[finished failed].include?(status) end # finished status means success, all other statuses are considered to fail the test - expect(status).to eq('finished') + expect(status).to eq('finished'), "Expected import to finish successfully, but status was: #{status}" end before do diff --git a/scripts/lib/glfm/update_specification.rb b/scripts/lib/glfm/update_specification.rb index 1a66928bae4..ef6f24d5a77 100644 --- a/scripts/lib/glfm/update_specification.rb +++ b/scripts/lib/glfm/update_specification.rb @@ -306,7 +306,7 @@ module Glfm end def write_spec_html(spec_html_string) - output("Writing #{GLFM_SPEC_TXT_PATH}...") + output("Writing #{GLFM_SPEC_HTML_PATH}...") FileUtils.mkdir_p(Pathname.new(GLFM_SPEC_HTML_PATH).dirname) write_file(GLFM_SPEC_HTML_PATH, "#{spec_html_string}\n") end diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb index 2605beac95a..a891a5c285b 100644 --- a/spec/graphql/types/environment_type_spec.rb +++ b/spec/graphql/types/environment_type_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Environment'] do specify { expect(described_class.graphql_name).to eq('Environment') } + specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Environment) } it 'includes the expected fields' do expected_fields = %w[ diff --git a/spec/graphql/types/permission_types/environment_spec.rb b/spec/graphql/types/permission_types/environment_spec.rb new file mode 100644 index 00000000000..944699c972a --- /dev/null +++ b/spec/graphql/types/permission_types/environment_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::PermissionTypes::Environment, feature_category: :continuous_delivery do + it do + expected_permissions = [ + :update_environment, :destroy_environment, :stop_environment + ] + + expected_permissions.each do |permission| + expect(described_class).to have_graphql_field(permission) + end + end +end diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb index e5b6aebbf2c..a86edde2ef3 100644 --- a/spec/requests/api/graphql/project/environments_spec.rb +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -47,6 +47,60 @@ RSpec.describe 'Project Environments query' do expect(environment_data['environmentType']).to eq(production.environment_type) end + describe 'user permissions' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{production.name}") { + userPermissions { + updateEnvironment + destroyEnvironment + stopEnvironment + } + } + } + } + ) + end + + it 'returns user permissions of the environment', :aggregate_failures do + subject + + permission_data = graphql_data.dig('project', 'environment', 'userPermissions') + expect(permission_data['updateEnvironment']).to eq(true) + expect(permission_data['destroyEnvironment']).to eq(false) + expect(permission_data['stopEnvironment']).to eq(true) + end + + context 'when fetching user permissions for multiple environments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environments { + nodes { + userPermissions { + updateEnvironment + destroyEnvironment + stopEnvironment + } + } + } + } + } + ) + end + + it 'limits the result', :aggregate_failures do + subject + + expect_graphql_errors_to_include('"userPermissions" field can be requested only ' \ + 'for 1 Environment(s) at a time.') + end + end + end + describe 'last deployments of environments' do ::Deployment.statuses.each do |status, _| let_it_be(:"production_#{status}_deployment") do |