Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb2
-rw-r--r--app/finders/ci/runners_finder.rb18
-rw-r--r--app/graphql/resolvers/ci/group_runners_resolver.rb26
-rw-r--r--app/graphql/resolvers/ci/runners_resolver.rb15
-rw-r--r--app/graphql/types/ci/runner_membership_filter_enum.rb18
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb1
-rw-r--r--config/feature_flags/development/create_vulnerabilities_via_api.yml (renamed from config/feature_flags/development/dast_meta_tag_validation.yml)12
-rw-r--r--config/feature_flags/development/dast_runner_site_validation.yml8
-rw-r--r--db/post_migrate/20210731132939_backfill_stage_event_hash.rb115
-rw-r--r--db/schema_migrations/202107311329391
-rw-r--r--db/structure.sql6
-rw-r--r--doc/api/graphql/reference/index.md103
-rw-r--r--doc/user/application_security/dast/index.md6
-rw-r--r--doc/user/application_security/img/vulnerability-check_v13_4.pngbin25832 -> 0 bytes
-rw-r--r--doc/user/application_security/img/vulnerability-check_v14_2.pngbin0 -> 23147 bytes
-rw-r--r--doc/user/application_security/index.md18
-rw-r--r--doc/user/infrastructure/index.md54
-rw-r--r--locale/gitlab.pot10
-rw-r--r--qa/qa/resource/issue.rb7
-rw-r--r--qa/qa/runtime/search.rb6
-rw-r--r--qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb37
-rw-r--r--spec/finders/ci/runners_finder_spec.rb126
-rw-r--r--spec/graphql/resolvers/ci/group_runners_resolver_spec.rb94
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb209
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb5
-rw-r--r--spec/migrations/backfill_stage_event_hash_spec.rb103
-rw-r--r--spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb22
30 files changed, 761 insertions, 271 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index c742253cb00..9d553e5d555 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-de019fc19eeb8bc6a65a6dbd8bf236669c777815
+2ed9a2c78ec556eb8d64e03203c864355ea5a128
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index dbbfdd76fe8..ff3a09a2d2d 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -59,7 +59,7 @@ class Groups::RunnersController < Groups::ApplicationController
private
def runner
- @runner ||= Ci::RunnersFinder.new(current_user: current_user, group: @group, params: {}).execute
+ @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
.except(:limit, :offset)
.find(params[:id])
end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 0f40c9bfd2c..a290ef9b5e7 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -17,7 +17,7 @@ module Groups
NUMBER_OF_RUNNERS_PER_PAGE = 4
def show
- runners_finder = Ci::RunnersFinder.new(current_user: current_user, group: @group, params: params)
+ runners_finder = Ci::RunnersFinder.new(current_user: current_user, params: params.merge({ group: @group }))
# We need all runners for count
@all_group_runners = runners_finder.execute.except(:limit, :offset)
@group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index d34b3202433..8bc2a47a024 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -7,9 +7,9 @@ module Ci
ALLOWED_SORTS = %w[contacted_asc contacted_desc created_at_asc created_at_desc created_date].freeze
DEFAULT_SORT = 'created_at_desc'
- def initialize(current_user:, group: nil, params:)
+ def initialize(current_user:, params:)
@params = params
- @group = group
+ @group = params.delete(:group)
@current_user = current_user
end
@@ -48,10 +48,16 @@ module Ci
def group_runners
raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group)
- # Getting all runners from the group itself and all its descendants
- descendant_projects = Project.for_group_and_its_subgroups(@group)
-
- @runners = Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
+ @runners = case @params[:membership]
+ when :direct
+ Ci::Runner.belonging_to_group(@group.id)
+ when :descendants, nil
+ # Getting all runners from the group itself and all its descendant groups/projects
+ descendant_projects = Project.for_group_and_its_subgroups(@group)
+ Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
+ else
+ raise ArgumentError, 'Invalid membership filter'
+ end
end
def filter_by_status!
diff --git a/app/graphql/resolvers/ci/group_runners_resolver.rb b/app/graphql/resolvers/ci/group_runners_resolver.rb
new file mode 100644
index 00000000000..e9c399d3855
--- /dev/null
+++ b/app/graphql/resolvers/ci/group_runners_resolver.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class GroupRunnersResolver < RunnersResolver
+ type Types::Ci::RunnerType.connection_type, null: true
+
+ argument :membership, ::Types::Ci::RunnerMembershipFilterEnum,
+ required: false,
+ default_value: :descendants,
+ description: 'Control which runners to include in the results.'
+
+ protected
+
+ def runners_finder_params(params)
+ super(params).merge(membership: params[:membership])
+ end
+
+ def parent_param
+ raise 'Expected group missing' unless parent.is_a?(Group)
+
+ { group: parent }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb
index 1957c4ec058..07105701daa 100644
--- a/app/graphql/resolvers/ci/runners_resolver.rb
+++ b/app/graphql/resolvers/ci/runners_resolver.rb
@@ -34,7 +34,7 @@ module Resolvers
.execute)
end
- private
+ protected
def runners_finder_params(params)
{
@@ -47,6 +47,19 @@ module Resolvers
tag_name: node_selection&.selects?(:tag_list)
}
}.compact
+ .merge(parent_param)
+ end
+
+ def parent_param
+ return {} unless parent
+
+ raise "Unexpected parent type: #{parent.class}"
+ end
+
+ private
+
+ def parent
+ object.respond_to?(:sync) ? object.sync : object
end
end
end
diff --git a/app/graphql/types/ci/runner_membership_filter_enum.rb b/app/graphql/types/ci/runner_membership_filter_enum.rb
new file mode 100644
index 00000000000..2e1051b2151
--- /dev/null
+++ b/app/graphql/types/ci/runner_membership_filter_enum.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerMembershipFilterEnum < BaseEnum
+ graphql_name 'RunnerMembershipFilter'
+ description 'Values for filtering runners in namespaces.'
+
+ value 'DIRECT',
+ description: "Include runners that have a direct relationship.",
+ value: :direct
+
+ value 'DESCENDANTS',
+ description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).",
+ value: :descendants
+ end
+ end
+end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index fbf0084cd0e..baf0fa80fc3 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -155,6 +155,12 @@ module Types
complexity: 5,
resolver: Resolvers::GroupsResolver
+ field :runners, Types::Ci::RunnerType.connection_type,
+ null: true,
+ resolver: Resolvers::Ci::GroupRunnersResolver,
+ description: "Find runners visible to the current user.",
+ feature_flag: :runner_graphql_query
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index 4dfe136c206..9bbc326a750 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -16,7 +16,6 @@ module Ci
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
- "commit-sha" => commit_sha,
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,
diff --git a/config/feature_flags/development/dast_meta_tag_validation.yml b/config/feature_flags/development/create_vulnerabilities_via_api.yml
index 50ef18df45a..0a3f9fa73f8 100644
--- a/config/feature_flags/development/dast_meta_tag_validation.yml
+++ b/config/feature_flags/development/create_vulnerabilities_via_api.yml
@@ -1,8 +1,8 @@
---
-name: dast_meta_tag_validation
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67945
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337711
-milestone: '14.2'
+name: create_vulnerabilities_via_api
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68158
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338694
+milestone: '14.3'
type: development
-group: group::dynamic analysis
-default_enabled: true
+group: group::threat insights
+default_enabled: false
diff --git a/config/feature_flags/development/dast_runner_site_validation.yml b/config/feature_flags/development/dast_runner_site_validation.yml
deleted file mode 100644
index e39a8a6d1e3..00000000000
--- a/config/feature_flags/development/dast_runner_site_validation.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: dast_runner_site_validation
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61649
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082
-milestone: '14.0'
-type: development
-group: group::dynamic analysis
-default_enabled: true
diff --git a/db/post_migrate/20210731132939_backfill_stage_event_hash.rb b/db/post_migrate/20210731132939_backfill_stage_event_hash.rb
new file mode 100644
index 00000000000..2c4dc904387
--- /dev/null
+++ b/db/post_migrate/20210731132939_backfill_stage_event_hash.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+class BackfillStageEventHash < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ BATCH_SIZE = 100
+ EVENT_ID_IDENTIFIER_MAPPING = {
+ 1 => :issue_created,
+ 2 => :issue_first_mentioned_in_commit,
+ 3 => :issue_closed,
+ 4 => :issue_first_added_to_board,
+ 5 => :issue_first_associated_with_milestone,
+ 7 => :issue_last_edited,
+ 8 => :issue_label_added,
+ 9 => :issue_label_removed,
+ 10 => :issue_deployed_to_production,
+ 100 => :merge_request_created,
+ 101 => :merge_request_first_deployed_to_production,
+ 102 => :merge_request_last_build_finished,
+ 103 => :merge_request_last_build_started,
+ 104 => :merge_request_merged,
+ 105 => :merge_request_closed,
+ 106 => :merge_request_last_edited,
+ 107 => :merge_request_label_added,
+ 108 => :merge_request_label_removed,
+ 109 => :merge_request_first_commit_at,
+ 1000 => :code_stage_start,
+ 1001 => :issue_stage_end,
+ 1002 => :plan_stage_start
+ }.freeze
+
+ LABEL_BASED_EVENTS = Set.new([8, 9, 107, 108]).freeze
+
+ class GroupStage < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'analytics_cycle_analytics_group_stages'
+ end
+
+ class ProjectStage < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'analytics_cycle_analytics_project_stages'
+ end
+
+ class StageEventHash < ActiveRecord::Base
+ self.table_name = 'analytics_cycle_analytics_stage_event_hashes'
+ end
+
+ def up
+ GroupStage.reset_column_information
+ ProjectStage.reset_column_information
+ StageEventHash.reset_column_information
+
+ update_stage_table(GroupStage)
+ update_stage_table(ProjectStage)
+
+ add_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
+ add_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
+ end
+
+ def down
+ remove_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
+ remove_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
+ end
+
+ private
+
+ def update_stage_table(klass)
+ klass.each_batch(of: BATCH_SIZE) do |relation|
+ klass.transaction do
+ records = relation.where(stage_event_hash_id: nil).lock!.to_a # prevent concurrent modification (unlikely to happen)
+ records = delete_invalid_records(records)
+ next if records.empty?
+
+ hashes_by_stage = records.to_h { |stage| [stage, calculate_stage_events_hash(stage)] }
+ hashes = hashes_by_stage.values.uniq
+
+ StageEventHash.insert_all(hashes.map { |hash| { hash_sha256: hash } })
+
+ stage_event_hashes_by_hash = StageEventHash.where(hash_sha256: hashes).index_by(&:hash_sha256)
+ records.each do |stage|
+ stage.update!(stage_event_hash_id: stage_event_hashes_by_hash[hashes_by_stage[stage]].id)
+ end
+ end
+ end
+ end
+
+ def calculate_stage_events_hash(stage)
+ start_event_hash = calculate_event_hash(stage.start_event_identifier, stage.start_event_label_id)
+ end_event_hash = calculate_event_hash(stage.end_event_identifier, stage.end_event_label_id)
+
+ Digest::SHA256.hexdigest("#{start_event_hash}-#{end_event_hash}")
+ end
+
+ def calculate_event_hash(event_identifier, label_id = nil)
+ str = EVENT_ID_IDENTIFIER_MAPPING.fetch(event_identifier).to_s
+ str << "-#{label_id}" if LABEL_BASED_EVENTS.include?(event_identifier)
+
+ Digest::SHA256.hexdigest(str)
+ end
+
+ # Invalid records are safe to delete, since they are not working properly anyway
+ def delete_invalid_records(records)
+ to_be_deleted = records.select do |record|
+ EVENT_ID_IDENTIFIER_MAPPING[record.start_event_identifier].nil? ||
+ EVENT_ID_IDENTIFIER_MAPPING[record.end_event_identifier].nil?
+ end
+
+ to_be_deleted.each(&:delete)
+ records - to_be_deleted
+ end
+end
diff --git a/db/schema_migrations/20210731132939 b/db/schema_migrations/20210731132939
new file mode 100644
index 00000000000..f032b0fadad
--- /dev/null
+++ b/db/schema_migrations/20210731132939
@@ -0,0 +1 @@
+97d968bba0eb2bf6faa19de8a3e4fe93dc03a623b623dc802ab0fe0a4afb0370 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 860b2d6f287..0710dc312c2 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9102,7 +9102,8 @@ CREATE TABLE analytics_cycle_analytics_group_stages (
custom boolean DEFAULT true NOT NULL,
name character varying(255) NOT NULL,
group_value_stream_id bigint NOT NULL,
- stage_event_hash_id bigint
+ stage_event_hash_id bigint,
+ CONSTRAINT check_e6bd4271b5 CHECK ((stage_event_hash_id IS NOT NULL))
);
CREATE SEQUENCE analytics_cycle_analytics_group_stages_id_seq
@@ -9146,7 +9147,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages (
custom boolean DEFAULT true NOT NULL,
name character varying(255) NOT NULL,
project_value_stream_id bigint NOT NULL,
- stage_event_hash_id bigint
+ stage_event_hash_id bigint,
+ CONSTRAINT check_8f6019de1e CHECK ((stage_event_hash_id IS NOT NULL))
);
CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6600901e187..4af7b8e9077 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4459,6 +4459,39 @@ Input type: `VulnerabilityConfirmInput`
| <a id="mutationvulnerabilityconfirmerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationvulnerabilityconfirmvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | The vulnerability after state change. |
+### `Mutation.vulnerabilityCreate`
+
+Input type: `VulnerabilityCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvulnerabilitycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvulnerabilitycreateconfidence"></a>`confidence` | [`VulnerabilityConfidence`](#vulnerabilityconfidence) | Confidence of the vulnerability (defaults to `unknown`). |
+| <a id="mutationvulnerabilitycreateconfirmedat"></a>`confirmedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to confirmed (defaults to creation time if status is `confirmed`). |
+| <a id="mutationvulnerabilitycreatedescription"></a>`description` | [`String!`](#string) | Description of the vulnerability. |
+| <a id="mutationvulnerabilitycreatedetectedat"></a>`detectedAt` | [`Time`](#time) | Timestamp of when the vulnerability was first detected (defaults to creation time). |
+| <a id="mutationvulnerabilitycreatedismissedat"></a>`dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to dismissed (defaults to creation time if status is `dismissed`). |
+| <a id="mutationvulnerabilitycreateidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifierInput!]!`](#vulnerabilityidentifierinput) | Array of CVE or CWE identifiers for the vulnerability. |
+| <a id="mutationvulnerabilitycreatemessage"></a>`message` | [`String`](#string) | Additional information about the vulnerability. |
+| <a id="mutationvulnerabilitycreateproject"></a>`project` | [`ProjectID!`](#projectid) | ID of the project to attach the vulnerability to. |
+| <a id="mutationvulnerabilitycreateresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to resolved (defaults to creation time if status is `resolved`). |
+| <a id="mutationvulnerabilitycreatescannername"></a>`scannerName` | [`String!`](#string) | Name of the security scanner used to discover the vulnerability. |
+| <a id="mutationvulnerabilitycreatescannertype"></a>`scannerType` | [`SecurityScannerType!`](#securityscannertype) | Type of the security scanner used to discover the vulnerability. |
+| <a id="mutationvulnerabilitycreateseverity"></a>`severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability (defaults to `unknown`). |
+| <a id="mutationvulnerabilitycreatesolution"></a>`solution` | [`String`](#string) | How to fix this vulnerability. |
+| <a id="mutationvulnerabilitycreatestate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | State of the vulnerability (defaults to `detected`). |
+| <a id="mutationvulnerabilitycreatetitle"></a>`title` | [`String!`](#string) | Title of the vulnerability. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationvulnerabilitycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationvulnerabilitycreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationvulnerabilitycreatevulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | Vulnerability created. |
+
### `Mutation.vulnerabilityDismiss`
Input type: `VulnerabilityDismissInput`
@@ -10004,6 +10037,27 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupprojectssearch"></a>`search` | [`String`](#string) | Search project with most similar names or paths. |
| <a id="groupprojectssort"></a>`sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. |
+##### `Group.runners`
+
+Find runners visible to the current user. Available only when feature flag `runner_graphql_query` is enabled. This flag is enabled by default.
+
+Returns [`CiRunnerConnection`](#cirunnerconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="grouprunnersmembership"></a>`membership` | [`RunnerMembershipFilter`](#runnermembershipfilter) | Control which runners to include in the results. |
+| <a id="grouprunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
+| <a id="grouprunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
+| <a id="grouprunnersstatus"></a>`status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. |
+| <a id="grouprunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
+| <a id="grouprunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
+
##### `Group.timelogs`
Time logged on issues and merge requests in the group and its subgroups.
@@ -13261,6 +13315,7 @@ Represents summary of a security report.
| <a id="securityreportsummarycoveragefuzzing"></a>`coverageFuzzing` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `coverage_fuzzing` scan. |
| <a id="securityreportsummarydast"></a>`dast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dast` scan. |
| <a id="securityreportsummarydependencyscanning"></a>`dependencyScanning` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dependency_scanning` scan. |
+| <a id="securityreportsummarygeneric"></a>`generic` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `generic` scan. |
| <a id="securityreportsummarysast"></a>`sast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `sast` scan. |
| <a id="securityreportsummarysecretdetection"></a>`secretDetection` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `secret_detection` scan. |
@@ -14141,7 +14196,7 @@ Represents a vulnerability.
| <a id="vulnerabilitynotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
| <a id="vulnerabilityprimaryidentifier"></a>`primaryIdentifier` | [`VulnerabilityIdentifier`](#vulnerabilityidentifier) | Primary identifier of the vulnerability. |
| <a id="vulnerabilityproject"></a>`project` | [`Project`](#project) | The project on which the vulnerability was found. |
-| <a id="vulnerabilityreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING, CLUSTER_IMAGE_SCANNING). `Scan Type` in the UI. |
+| <a id="vulnerabilityreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING, CLUSTER_IMAGE_SCANNING, GENERIC). `Scan Type` in the UI. |
| <a id="vulnerabilityresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to resolved. |
| <a id="vulnerabilityresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | The user that resolved the vulnerability. |
| <a id="vulnerabilityresolvedondefaultbranch"></a>`resolvedOnDefaultBranch` | [`Boolean!`](#boolean) | Indicates whether the vulnerability is fixed on the default branch or not. |
@@ -14435,6 +14490,16 @@ Represents the location of a vulnerability found by a dependency security scan.
| <a id="vulnerabilitylocationdependencyscanningdependency"></a>`dependency` | [`VulnerableDependency`](#vulnerabledependency) | Dependency containing the vulnerability. |
| <a id="vulnerabilitylocationdependencyscanningfile"></a>`file` | [`String`](#string) | Path to the vulnerable file. |
+### `VulnerabilityLocationGeneric`
+
+Represents the location of a vulnerability found by a generic scanner.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="vulnerabilitylocationgenericdescription"></a>`description` | [`String`](#string) | Free-form description of where the vulnerability is located. |
+
### `VulnerabilityLocationSast`
Represents the location of a vulnerability found by a SAST scan.
@@ -15626,6 +15691,15 @@ Status of a requirement based on last test report.
| <a id="requirementstatusfiltermissing"></a>`MISSING` | Requirements without any test report. |
| <a id="requirementstatusfilterpassed"></a>`PASSED` | |
+### `RunnerMembershipFilter`
+
+Values for filtering runners in namespaces.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="runnermembershipfilterdescendants"></a>`DESCENDANTS` | Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried). |
+| <a id="runnermembershipfilterdirect"></a>`DIRECT` | Include runners that have a direct relationship. |
+
### `SastUiComponentSize`
Size of UI component in SAST configuration page.
@@ -15872,6 +15946,20 @@ Possible states of a user.
| <a id="visibilityscopesenumprivate"></a>`private` | The snippet is visible only to the snippet creator. |
| <a id="visibilityscopesenumpublic"></a>`public` | The snippet can be accessed without any authentication. |
+### `VulnerabilityConfidence`
+
+Confidence that a given vulnerability is present in the codebase.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="vulnerabilityconfidenceconfirmed"></a>`CONFIRMED` | |
+| <a id="vulnerabilityconfidenceexperimental"></a>`EXPERIMENTAL` | |
+| <a id="vulnerabilityconfidencehigh"></a>`HIGH` | |
+| <a id="vulnerabilityconfidenceignore"></a>`IGNORE` | |
+| <a id="vulnerabilityconfidencelow"></a>`LOW` | |
+| <a id="vulnerabilityconfidencemedium"></a>`MEDIUM` | |
+| <a id="vulnerabilityconfidenceunknown"></a>`UNKNOWN` | |
+
### `VulnerabilityDismissalReason`
The dismissal reason of the Vulnerability.
@@ -15933,6 +16021,7 @@ The type of the security scan that found the vulnerability.
| <a id="vulnerabilityreporttypecoverage_fuzzing"></a>`COVERAGE_FUZZING` | |
| <a id="vulnerabilityreporttypedast"></a>`DAST` | |
| <a id="vulnerabilityreporttypedependency_scanning"></a>`DEPENDENCY_SCANNING` | |
+| <a id="vulnerabilityreporttypegeneric"></a>`GENERIC` | |
| <a id="vulnerabilityreporttypesast"></a>`SAST` | |
| <a id="vulnerabilityreporttypesecret_detection"></a>`SECRET_DETECTION` | |
@@ -16573,6 +16662,7 @@ One of:
- [`VulnerabilityLocationCoverageFuzzing`](#vulnerabilitylocationcoveragefuzzing)
- [`VulnerabilityLocationDast`](#vulnerabilitylocationdast)
- [`VulnerabilityLocationDependencyScanning`](#vulnerabilitylocationdependencyscanning)
+- [`VulnerabilityLocationGeneric`](#vulnerabilitylocationgeneric)
- [`VulnerabilityLocationSast`](#vulnerabilitylocationsast)
- [`VulnerabilityLocationSecretDetection`](#vulnerabilitylocationsecretdetection)
@@ -17351,3 +17441,14 @@ A time-frame defined as a closed inclusive range of two dates.
| <a id="updatediffimagepositioninputwidth"></a>`width` | [`Int`](#int) | Total width of the image. |
| <a id="updatediffimagepositioninputx"></a>`x` | [`Int`](#int) | X position of the note. |
| <a id="updatediffimagepositioninputy"></a>`y` | [`Int`](#int) | Y position of the note. |
+
+### `VulnerabilityIdentifierInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="vulnerabilityidentifierinputexternalid"></a>`externalId` | [`String`](#string) | External ID of the vulnerability identifier. |
+| <a id="vulnerabilityidentifierinputexternaltype"></a>`externalType` | [`String`](#string) | External type of the vulnerability identifier. |
+| <a id="vulnerabilityidentifierinputname"></a>`name` | [`String!`](#string) | Name of the vulnerability identifier. |
+| <a id="vulnerabilityidentifierinputurl"></a>`url` | [`String!`](#string) | URL of the vulnerability identifier. |
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 7455915761c..9a83b077f4e 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -1049,11 +1049,7 @@ When an API site type is selected, a [host override](#host-override) is used to
#### Site profile validation
> - Site profile validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233020) in GitLab 13.8.
-> - Meta tag validation [enabled on GitLab.com](https://gitlab.com/groups/gitlab-org/-/epics/6460) in GitLab 14.2 and is ready for production use.
-> - Meta tag validation [enabled with `dast_meta_tag_validation flag` flag](https://gitlab.com/gitlab-org/gitlab/-/issues/337711) for self-managed GitLab in GitLab 14.2 and is ready for production use.
-
-FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the `dast_meta_tag_validation` flag](../../../administration/feature_flags.md). On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
+> - Meta tag validation [introduced](https://gitlab.com/groups/gitlab-org/-/epics/6460) in GitLab 14.2.
Site profile validation reduces the risk of running an active scan against the wrong website. A site
must be validated before an active scan can run against it. The site validation methods are as
diff --git a/doc/user/application_security/img/vulnerability-check_v13_4.png b/doc/user/application_security/img/vulnerability-check_v13_4.png
deleted file mode 100644
index 3e38f6eebe7..00000000000
--- a/doc/user/application_security/img/vulnerability-check_v13_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/img/vulnerability-check_v14_2.png b/doc/user/application_security/img/vulnerability-check_v14_2.png
new file mode 100644
index 00000000000..655e43221c7
--- /dev/null
+++ b/doc/user/application_security/img/vulnerability-check_v14_2.png
Binary files differ
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 3b0725021ef..50fd727b892 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -194,14 +194,19 @@ merge request would introduce one of the following security issues:
When the Vulnerability-Check merge request rule is enabled, additional merge request approval
is required when the latest security report in a merge request:
-- Contains a vulnerability of `high`, `critical`, or `unknown` severity that is not present in the
+- Contains vulnerabilities that are not present in the
target branch. Note that approval is still required for dismissed vulnerabilities.
+- Contains vulnerabilities with severity levels (for example, `high`, `critical`, or `unknown`)
+ matching the rule's severity levels.
+- Contains a vulnerability count higher than the rule allows.
- Is not generated during pipeline execution.
An approval is optional when the security report:
- Contains no new vulnerabilities when compared to the target branch.
-- Contains only new vulnerabilities of `low` or `medium` severity.
+- Contains only vulnerabilities with severity levels (for example, `low`, `medium`) **NOT** matching
+ the rule's severity levels.
+- Contains a vulnerability count equal to or less than what the rule allows.
When the License-Check merge request rule is enabled, additional approval is required if a merge
request contains a denied license. For more details, see [Enabling license approvals within a project](../compliance/license_compliance/index.md#enabling-license-approvals-within-a-project).
@@ -219,16 +224,19 @@ Follow these steps to enable `Vulnerability-Check`:
1. Go to your project and select **Settings > General**.
1. Expand **Merge request approvals**.
1. Select **Enable** or **Edit**.
-1. Add or change the **Rule name** to `Vulnerability-Check` (case sensitive).
-1. Set the **No. of approvals required** to greater than zero.
+1. Set the **Security scanners** that the rule applies to.
1. Select the **Target branch**.
+1. Set the **Vulnerabilities allowed** to the number of vulnerabilities allowed before the rule is
+ triggered.
+1. Set the **Severity levels** to the severity levels that the rule applies to.
+1. Set the **Approvals required** to the number of approvals that the rule requires.
1. Select the users or groups to provide approval.
1. Select **Add approval rule**.
Once this group is added to your project, the approval rule is enabled for all merge requests.
Any code changes cause the approvals required to reset.
-![Vulnerability Check Approver Rule](img/vulnerability-check_v13_4.png)
+![Vulnerability Check Approver Rule](img/vulnerability-check_v14_2.png)
## Using private Maven repositories
diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md
index b2d75a22615..f3f9f648e5a 100644
--- a/doc/user/infrastructure/index.md
+++ b/doc/user/infrastructure/index.md
@@ -6,9 +6,53 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Infrastructure management **(FREE)**
-GitLab provides you with great solutions to help you manage your
-infrastructure:
+With the rise of DevOps and SRE approaches, infrastructure management becomes codified,
+automatable, and software development best practices gain their place around infrastructure
+management too. On one hand, the daily tasks of classical operations people changed
+and are more similar to traditional software development. On the other hand, software engineers
+are more likely to control their whole DevOps lifecycle, including deployments and delivery.
-- [Infrastructure as Code and GitOps](iac/index.md)
-- [Kubernetes clusters](../project/clusters/index.md)
-- [Runbooks](../project/clusters/runbooks/index.md)
+GitLab offers various features to speed up and simplify your infrastructure management practices.
+
+## Generic infrastructure management
+
+GitLab has deep integrations with Terraform to run your infrastructure as code pipelines
+and support your processes. Terraform is considered the standard in cloud infrastructure provisioning.
+The various GitLab integrations help you:
+
+- Get started quickly without any setup.
+- Collaborate around infrastructure changes in merge requests the same as you might
+ with code changes.
+- Scale using a module registry.
+
+Read more about the [Infrastructure as Code features](iac/index.md), including:
+
+- [The GitLab Managed Terraform State](terraform_state.md).
+- [The Terraform MR widget](mr_integration.md).
+- [The Terraform module registry](../packages/terraform_module_registry/index.md).
+
+## Integrated Kubernetes management
+
+GitLab has special integrations with Kubernetes to help you deploy, manage and troubleshoot
+third-party or custom applications in Kubernetes clusters. Auto DevOps provides a full
+DevSecOps pipeline by default targeted at Kubernetes based deployments. To support
+all the GitLab features, GitLab offers a cluster management project for easy onboarding.
+The deploy boards provide quick insights into your cluster, including pod logs tailing.
+
+The recommended approach to connect to a cluster is using [the GitLab Kubernetes Agent](../clusters/agent/index.md).
+
+Read more about [the Kubernetes cluster support and integrations](../project/clusters/index.md), including:
+
+- Certificate-based integration for [projects](../project/clusters/index.md),
+ [groups](../group/clusters/index.md), or [instances](../instance/clusters/index.md).
+- [Agent-based integration](../clusters/agent/index.md). **(PREMIUM)**
+ - The [Kubernetes Agent Server](../../administration/clusters/kas.md) is [available on GitLab.com](../clusters/agent/index.md#set-up-the-kubernetes-agent-server)
+ at `wss://kas.gitlab.com`. **(PREMIUM)**
+- [Agent-based access from GitLab CI/CD](../clusters/agent/ci_cd_tunnel.md).
+
+## Runbooks in GitLab
+
+Runbooks are a collection of documented procedures that explain how to carry out a task,
+such as starting, stopping, debugging, or troubleshooting a system.
+
+Read more about [how executable runbooks work in GitLab](../project/clusters/runbooks/index.md).
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 886e476df0b..934f51254db 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -29472,7 +29472,7 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability of high, critical, or unknown severity."
+msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
@@ -29508,7 +29508,7 @@ msgstr ""
msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}More information%{linkEnd}"
msgstr ""
-msgid "SecurityApprovals|Requires approval for vulnerabilities of Critical, High, or Unknown severity. %{linkStart}Learn more.%{linkEnd}"
+msgid "SecurityApprovals|Requires approval for vulnerabilities. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityApprovals|Test coverage must be enabled. %{linkStart}Learn more%{linkEnd}."
@@ -37184,9 +37184,6 @@ msgstr ""
msgid "Vulnerability|Request/Response"
msgstr ""
-msgid "Vulnerability|Scanner"
-msgstr ""
-
msgid "Vulnerability|Scanner Provider"
msgstr ""
@@ -37199,6 +37196,9 @@ msgstr ""
msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request"
msgstr ""
+msgid "Vulnerability|Tool"
+msgstr ""
+
msgid "Vulnerability|Unmodified Response"
msgstr ""
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index c45ab7593b6..d20813e9f2a 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -18,12 +18,14 @@ module QA
:iid,
:assignee_ids,
:labels,
- :title
+ :title,
+ :description
def initialize
@assignee_ids = []
@labels = []
@title = "Issue title #{SecureRandom.hex(8)}"
+ @description = "Issue description #{SecureRandom.hex(8)}"
end
def fabricate!
@@ -34,7 +36,7 @@ module QA
Page::Project::Issue::New.perform do |new_page|
new_page.fill_title(@title)
new_page.choose_template(@template) if @template
- new_page.fill_description(@description) if @description
+ new_page.fill_description(@description) if @description && !@template
new_page.choose_milestone(@milestone) if @milestone
new_page.create_new_issue
end
@@ -64,6 +66,7 @@ module QA
}.tap do |hash|
hash[:milestone_id] = @milestone.id if @milestone
hash[:weight] = @weight if @weight
+ hash[:description] = @description if @description
end
end
diff --git a/qa/qa/runtime/search.rb b/qa/qa/runtime/search.rb
index f7f87d96e68..2a5db97cdad 100644
--- a/qa/qa/runtime/search.rb
+++ b/qa/qa/runtime/search.rb
@@ -8,6 +8,10 @@ module QA
extend self
extend Support::Api
+ RETRY_MAX_ITERATION = 10
+ RETRY_SLEEP_INTERVAL = 12
+ INSERT_RECALL_THRESHOLD = RETRY_MAX_ITERATION * RETRY_SLEEP_INTERVAL
+
ElasticSearchServerError = Class.new(RuntimeError)
def assert_elasticsearch_responding
@@ -85,7 +89,7 @@ module QA
private
def find_target_in_scope(scope, search_term)
- QA::Support::Retrier.retry_until(max_attempts: 10, sleep_interval: 10, raise_on_failure: true, retry_on_exception: true) do
+ QA::Support::Retrier.retry_until(max_attempts: RETRY_MAX_ITERATION, sleep_interval: RETRY_SLEEP_INTERVAL, raise_on_failure: true, retry_on_exception: true) do
result = search(scope, search_term)
result && result.any? { |record| yield record }
end
diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
index 385908f2176..69222a23275 100644
--- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
@@ -133,7 +133,7 @@ module QA
it 'imports large Github repo via api' do
start = Time.now
- imported_project # import the project
+ Runtime::Logger.info("Importing project '#{imported_project.full_path}'") # import the project and log path
fetch_github_objects # fetch all objects right after import has started
import_status = lambda do
@@ -221,32 +221,39 @@ module QA
# @return [void]
def verify_mrs_or_issues(type)
msg = ->(title) { "expected #{type} with title '#{title}' to have" }
+
+ # Compare length to have easy to read overview how many objects are missing
expected = type == 'mr' ? mrs : gl_issues
actual = type == 'mr' ? gh_prs : gh_issues
+ count_msg = "Expected to contain same amount of #{type}s. Gitlab: #{expected.length}, Github: #{actual.length}"
+ expect(expected.length).to eq(actual.length), count_msg
- # Compare length to have easy to read overview how many objects are missing
- expect(expected.length).to(
- eq(actual.length),
- "Expected to contain same amount of #{type}s. Expected: #{expected.length}, actual: #{actual.length}"
- )
logger.debug("= Comparing #{type}s =")
actual.each do |title, actual_item|
print "." # indicate that it is still going but don't spam the output with newlines
expected_item = expected[title]
+ # Print title in the error message to see which object is missing
expect(expected_item).to be_truthy, "#{msg.call(title)} been imported"
next unless expected_item
- expect(expected_item[:body]).to(
- include(actual_item[:body]),
- "#{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}"
- )
- expect(expected_item[:comments].length).to(
- eq(actual_item[:comments].length),
- "#{msg.call(title)} same amount of comments"
- )
- expect(expected_item[:comments]).to match_array(actual_item[:comments])
+ # Print difference in the description
+ expected_body = expected_item[:body]
+ actual_body = actual_item[:body]
+ body_msg = <<~MSG
+ #{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}
+ MSG
+ expect(expected_body).to include(actual_body), body_msg
+
+ # Print amount difference first
+ expected_comments = expected_item[:comments]
+ actual_comments = actual_item[:comments]
+ comment_count_msg = <<~MSG
+ #{msg.call(title)} same amount of comments. Gitlab: #{expected_comments.length}, Github: #{actual_comments.length}
+ MSG
+ expect(expected_comments.length).to eq(actual_comments.length), comment_count_msg
+ expect(expected_comments).to match_array(actual_comments)
end
puts # print newline after last print to make output pretty
end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 599b4ffb804..10d3f641e02 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -18,6 +18,13 @@ RSpec.describe Ci::RunnersFinder do
end
end
+ context 'with nil group' do
+ it 'returns all runners' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2]
+ end
+ end
+
context 'with preload param set to :tag_name true' do
it 'requests tags' do
expect(Ci::Runner).to receive(:with_tags).and_call_original
@@ -158,6 +165,7 @@ RSpec.describe Ci::RunnersFinder do
let_it_be(:project_4) { create(:project, group: sub_group_2) }
let_it_be(:project_5) { create(:project, group: sub_group_3) }
let_it_be(:project_6) { create(:project, group: sub_group_4) }
+ let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) }
let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago) }
let_it_be(:runner_sub_group_1) { create(:ci_runner, :group, active: false, contacted_at: 11.minutes.ago) }
let_it_be(:runner_sub_group_2) { create(:ci_runner, :group, contacted_at: 10.minutes.ago) }
@@ -171,7 +179,10 @@ RSpec.describe Ci::RunnersFinder do
let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5])}
let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6])}
- let(:params) { {} }
+ let(:target_group) { nil }
+ let(:membership) { nil }
+ let(:extra_params) { {} }
+ let(:params) { { group: target_group, membership: membership }.merge(extra_params).reject { |_, v| v.nil? } }
before do
group.runners << runner_group
@@ -182,65 +193,104 @@ RSpec.describe Ci::RunnersFinder do
end
describe '#execute' do
- subject { described_class.new(current_user: user, group: group, params: params).execute }
+ subject { described_class.new(current_user: user, params: params).execute }
+
+ shared_examples 'membership equal to :descendants' do
+ it 'returns all descendant runners' do
+ expect(subject).to eq([runner_project_7, runner_project_6, runner_project_5,
+ runner_project_4, runner_project_3, runner_project_2,
+ runner_project_1, runner_sub_group_4, runner_sub_group_3,
+ runner_sub_group_2, runner_sub_group_1, runner_group])
+ end
+ end
context 'with user as group owner' do
before do
group.add_owner(user)
end
- context 'passing no params' do
- it 'returns all descendant runners' do
- expect(subject).to eq([runner_project_7, runner_project_6, runner_project_5,
- runner_project_4, runner_project_3, runner_project_2,
- runner_project_1, runner_sub_group_4, runner_sub_group_3,
- runner_sub_group_2, runner_sub_group_1, runner_group])
+ context 'with :group as target group' do
+ let(:target_group) { group }
+
+ context 'passing no params' do
+ it_behaves_like 'membership equal to :descendants'
end
- end
- context 'with sort param' do
- let(:params) { { sort: 'contacted_asc' } }
+ context 'with :descendants membership' do
+ let(:membership) { :descendants }
- it 'sorts by specified attribute' do
- expect(subject).to eq([runner_group, runner_sub_group_1, runner_sub_group_2,
- runner_sub_group_3, runner_sub_group_4, runner_project_1,
- runner_project_2, runner_project_3, runner_project_4,
- runner_project_5, runner_project_6, runner_project_7])
+ it_behaves_like 'membership equal to :descendants'
end
- end
- context 'filtering' do
- context 'by search term' do
- let(:params) { { search: 'runner_project_search' } }
+ context 'with :direct membership' do
+ let(:membership) { :direct }
+
+ it 'returns runners belonging to group' do
+ expect(subject).to eq([runner_group])
+ end
+ end
+
+ context 'with unknown membership' do
+ let(:membership) { :unsupported }
- it 'returns correct runner' do
- expect(subject).to eq([runner_project_3])
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, 'Invalid membership filter')
end
end
- context 'by status' do
- let(:params) { { status_status: 'paused' } }
+ context 'with nil group' do
+ let(:target_group) { nil }
- it 'returns correct runner' do
- expect(subject).to eq([runner_sub_group_1])
+ it 'returns no runners' do
+ # Query should run against all runners, however since user is not admin, query returns no results
+ expect(subject).to eq([])
end
end
- context 'by tag_name' do
- let(:params) { { tag_name: %w[runner_tag] } }
+ context 'with sort param' do
+ let(:extra_params) { { sort: 'contacted_asc' } }
- it 'returns correct runner' do
- expect(subject).to eq([runner_project_5])
+ it 'sorts by specified attribute' do
+ expect(subject).to eq([runner_group, runner_sub_group_1, runner_sub_group_2,
+ runner_sub_group_3, runner_sub_group_4, runner_project_1,
+ runner_project_2, runner_project_3, runner_project_4,
+ runner_project_5, runner_project_6, runner_project_7])
end
end
- context 'by runner type' do
- let(:params) { { type_type: 'project_type' } }
+ context 'filtering' do
+ context 'by search term' do
+ let(:extra_params) { { search: 'runner_project_search' } }
+
+ it 'returns correct runner' do
+ expect(subject).to eq([runner_project_3])
+ end
+ end
+
+ context 'by status' do
+ let(:extra_params) { { status_status: 'paused' } }
+
+ it 'returns correct runner' do
+ expect(subject).to eq([runner_sub_group_1])
+ end
+ end
+
+ context 'by tag_name' do
+ let(:extra_params) { { tag_name: %w[runner_tag] } }
+
+ it 'returns correct runner' do
+ expect(subject).to eq([runner_project_5])
+ end
+ end
+
+ context 'by runner type' do
+ let(:extra_params) { { type_type: 'project_type' } }
- it 'returns correct runners' do
- expect(subject).to eq([runner_project_7, runner_project_6,
- runner_project_5, runner_project_4,
- runner_project_3, runner_project_2, runner_project_1])
+ it 'returns correct runners' do
+ expect(subject).to eq([runner_project_7, runner_project_6,
+ runner_project_5, runner_project_4,
+ runner_project_3, runner_project_2, runner_project_1])
+ end
end
end
end
@@ -278,7 +328,7 @@ RSpec.describe Ci::RunnersFinder do
end
describe '#sort_key' do
- subject { described_class.new(current_user: user, group: group, params: params).sort_key }
+ subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key }
context 'without params' do
it 'returns created_at_desc' do
@@ -287,7 +337,7 @@ RSpec.describe Ci::RunnersFinder do
end
context 'with params' do
- let(:params) { { sort: 'contacted_asc' } }
+ let(:extra_params) { { sort: 'contacted_asc' } }
it 'returns contacted_asc' do
expect(subject).to eq('contacted_asc')
diff --git a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
new file mode 100644
index 00000000000..89a2437a189
--- /dev/null
+++ b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::GroupRunnersResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) }
+
+ include_context 'runners resolver setup'
+
+ let(:obj) { group }
+ let(:args) { {} }
+
+ # First, we can do a couple of basic real tests to verify common cases. That ensures that the code works.
+ context 'when user cannot see runners' do
+ it 'returns no runners' do
+ expect(subject.items.to_a).to eq([])
+ end
+ end
+
+ context 'with user as group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'returns all the runners' do
+ expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner)
+ end
+
+ context 'with membership direct' do
+ let(:args) { { membership: :direct } }
+
+ it 'returns only direct runners' do
+ expect(subject.items.to_a).to contain_exactly(group_runner)
+ end
+ end
+ end
+
+ # Then, we can check specific edge cases for this resolver
+ context 'with obj set to nil' do
+ let(:obj) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Expected group missing')
+ end
+ end
+
+ context 'with obj not set to group' do
+ let(:obj) { build(:project) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Expected group missing')
+ end
+ end
+
+ # Here we have a mocked part. We assume that all possible edge cases are covered in RunnersFinder spec. So we don't need to test them twice.
+ # Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back.
+ describe 'Allowed query arguments' do
+ let(:finder) { instance_double(::Ci::RunnersFinder) }
+ let(:args) do
+ {
+ status: 'active',
+ type: :group_type,
+ tag_list: ['active_runner'],
+ search: 'abc',
+ sort: :contacted_asc,
+ membership: :descendants
+ }
+ end
+
+ let(:expected_params) do
+ {
+ status_status: 'active',
+ type_type: :group_type,
+ tag_name: ['active_runner'],
+ preload: { tag_name: nil },
+ search: 'abc',
+ sort: 'contacted_asc',
+ membership: :descendants,
+ group: group
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(subject.items.to_a).to eq([:execute_return_value])
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index 5ac15d5729f..bb8dadeca40 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -5,185 +5,70 @@ require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnersResolver do
include GraphqlHelpers
- let_it_be(:user) { create_default(:user, :admin) }
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:project) { create(:project, :repository, :public) }
-
- let_it_be(:inactive_project_runner) do
- create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
- end
-
- let_it_be(:offline_project_runner) do
- create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
- end
-
- let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 1.second.ago) }
- let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
-
describe '#resolve' do
- subject { resolve(described_class, ctx: { current_user: user }, args: args).items.to_a }
-
- let(:args) do
- {}
- end
-
- context 'when the user cannot see runners' do
- let(:user) { create(:user) }
-
- it 'returns no runners' do
- is_expected.to be_empty
- end
- end
-
- context 'without sort' do
- it 'returns all the runners' do
- is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner)
- end
- end
-
- context 'with a sort argument' do
- context "set to :contacted_asc" do
- let(:args) do
- { sort: :contacted_asc }
- end
-
- it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner]) }
- end
-
- context "set to :contacted_desc" do
- let(:args) do
- { sort: :contacted_desc }
- end
-
- it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner].reverse) }
- end
-
- context "set to :created_at_desc" do
- let(:args) do
- { sort: :created_at_desc }
- end
+ let(:obj) { nil }
+ let(:args) { {} }
- it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner]) }
- end
-
- context "set to :created_at_asc" do
- let(:args) do
- { sort: :created_at_asc }
- end
-
- it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner].reverse) }
- end
- end
+ subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) }
- context 'when type is filtered' do
- let(:args) do
- { type: runner_type.to_s }
- end
+ include_context 'runners resolver setup'
- context 'to instance runners' do
- let(:runner_type) { :instance_type }
+ # First, we can do a couple of basic real tests to verify common cases. That ensures that the code works.
+ context 'when user cannot see runners' do
+ let(:user) { build(:user) }
- it 'returns the instance runner' do
- is_expected.to contain_exactly(instance_runner)
- end
- end
-
- context 'to group runners' do
- let(:runner_type) { :group_type }
-
- it 'returns the group runner' do
- is_expected.to contain_exactly(group_runner)
- end
- end
-
- context 'to project runners' do
- let(:runner_type) { :project_type }
-
- it 'returns the project runner' do
- is_expected.to contain_exactly(inactive_project_runner, offline_project_runner)
- end
+ it 'returns no runners' do
+ expect(subject.items.to_a).to eq([])
end
end
- context 'when status is filtered' do
- let(:args) do
- { status: runner_status.to_s }
- end
-
- context 'to active runners' do
- let(:runner_status) { :active }
-
- it 'returns the instance and group runners' do
- is_expected.to contain_exactly(offline_project_runner, group_runner, instance_runner)
- end
- end
-
- context 'to offline runners' do
- let(:runner_status) { :offline }
+ context 'when user can see runners' do
+ let(:obj) { nil }
- it 'returns the offline project runner' do
- is_expected.to contain_exactly(offline_project_runner)
- end
+ it 'returns all the runners' do
+ expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
end
end
- context 'when tag list is filtered' do
- let(:args) do
- { tag_list: tag_list }
- end
-
- context 'with "project_runner" tag' do
- let(:tag_list) { ['project_runner'] }
+ # Then, we can check specific edge cases for this resolver
+ context 'with obj not set to nil' do
+ let(:obj) { build(:project) }
- it 'returns the project_runner runners' do
- is_expected.to contain_exactly(offline_project_runner, inactive_project_runner)
- end
- end
-
- context 'with "project_runner" and "active_runner" tags as comma-separated string' do
- let(:tag_list) { ['project_runner,active_runner'] }
-
- it 'returns the offline_project_runner runner' do
- is_expected.to contain_exactly(offline_project_runner)
- end
- end
-
- context 'with "active_runner" and "instance_runner" tags as array' do
- let(:tag_list) { %w[instance_runner active_runner] }
-
- it 'returns the offline_project_runner runner' do
- is_expected.to contain_exactly(instance_runner)
- end
+ it 'raises an error' do
+ expect { subject }.to raise_error(a_string_including('Unexpected parent type'))
end
end
- context 'when text is filtered' do
+ # Here we have a mocked part. We assume that all possible edge cases are covered in RunnersFinder spec. So we don't need to test them twice.
+ # Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back.
+ describe 'Allowed query arguments' do
+ let(:finder) { instance_double(::Ci::RunnersFinder) }
let(:args) do
- { search: search_term }
- end
-
- context 'to "project"' do
- let(:search_term) { 'project' }
-
- it 'returns both project runners' do
- is_expected.to contain_exactly(inactive_project_runner, offline_project_runner)
- end
- end
-
- context 'to "group"' do
- let(:search_term) { 'group' }
-
- it 'returns group runner' do
- is_expected.to contain_exactly(group_runner)
- end
- end
-
- context 'to "defghi"' do
- let(:search_term) { 'defghi' }
-
- it 'returns runners containing term in token' do
- is_expected.to contain_exactly(offline_project_runner)
- end
+ {
+ status: 'active',
+ type: :instance_type,
+ tag_list: ['active_runner'],
+ search: 'abc',
+ sort: :contacted_asc
+ }
+ end
+
+ let(:expected_params) do
+ {
+ status_status: 'active',
+ type_type: :instance_type,
+ tag_name: ['active_runner'],
+ preload: { tag_name: nil },
+ search: 'abc',
+ sort: 'contacted_asc'
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return([:execute_return_value])
+
+ expect(subject.items.to_a).to eq([:execute_return_value])
end
end
end
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index 3183a0a2394..874937bc4ce 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -42,7 +42,6 @@ RSpec.describe Ci::PipelineEditorHelper do
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
- "commit-sha" => project.commit.sha,
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'foo',
"initial-branch-name" => nil,
@@ -69,7 +68,6 @@ RSpec.describe Ci::PipelineEditorHelper do
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
- "commit-sha" => '',
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'foo',
"initial-branch-name" => nil,
@@ -97,10 +95,7 @@ RSpec.describe Ci::PipelineEditorHelper do
end
it 'returns correct values' do
- latest_feature_sha = project.repository.commit('feature').sha
-
expect(pipeline_editor_data['initial-branch-name']).to eq('feature')
- expect(pipeline_editor_data['commit-sha']).to eq(latest_feature_sha)
end
end
end
diff --git a/spec/migrations/backfill_stage_event_hash_spec.rb b/spec/migrations/backfill_stage_event_hash_spec.rb
new file mode 100644
index 00000000000..cecaddcd3d4
--- /dev/null
+++ b/spec/migrations/backfill_stage_event_hash_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe BackfillStageEventHash, schema: 20210730103808 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:labels) { table(:labels) }
+ let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
+ let(:project_stages) { table(:analytics_cycle_analytics_project_stages) }
+ let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
+ let(:project_value_streams) { table(:analytics_cycle_analytics_project_value_streams) }
+ let(:stage_event_hashes) { table(:analytics_cycle_analytics_stage_event_hashes) }
+
+ let(:issue_created) { 1 }
+ let(:issue_closed) { 3 }
+ let(:issue_label_removed) { 9 }
+ let(:unknown_stage_event) { -1 }
+
+ let(:namespace) { namespaces.create!(name: 'ns', path: 'ns', type: 'Group') }
+ let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
+ let(:group_label) { labels.create!(title: 'label', type: 'GroupLabel', group_id: namespace.id) }
+ let(:group_value_stream) { group_value_streams.create!(name: 'group vs', group_id: namespace.id) }
+ let(:project_value_stream) { project_value_streams.create!(name: 'project vs', project_id: project.id) }
+
+ let(:group_stage_1) do
+ group_stages.create!(
+ name: 'stage 1',
+ group_id: namespace.id,
+ start_event_identifier: issue_created,
+ end_event_identifier: issue_closed,
+ group_value_stream_id: group_value_stream.id
+ )
+ end
+
+ let(:group_stage_2) do
+ group_stages.create!(
+ name: 'stage 2',
+ group_id: namespace.id,
+ start_event_identifier: issue_created,
+ end_event_identifier: issue_label_removed,
+ end_event_label_id: group_label.id,
+ group_value_stream_id: group_value_stream.id
+ )
+ end
+
+ let(:project_stage_1) do
+ project_stages.create!(
+ name: 'stage 1',
+ project_id: project.id,
+ start_event_identifier: issue_created,
+ end_event_identifier: issue_closed,
+ project_value_stream_id: project_value_stream.id
+ )
+ end
+
+ let(:invalid_group_stage) do
+ group_stages.create!(
+ name: 'stage 3',
+ group_id: namespace.id,
+ start_event_identifier: issue_created,
+ end_event_identifier: unknown_stage_event,
+ group_value_stream_id: group_value_stream.id
+ )
+ end
+
+ describe '#up' do
+ it 'populates stage_event_hash_id column' do
+ group_stage_1
+ group_stage_2
+ project_stage_1
+
+ migrate!
+
+ group_stage_1.reload
+ group_stage_2.reload
+ project_stage_1.reload
+
+ expect(group_stage_1.stage_event_hash_id).not_to be_nil
+ expect(group_stage_2.stage_event_hash_id).not_to be_nil
+ expect(project_stage_1.stage_event_hash_id).not_to be_nil
+
+ expect(stage_event_hashes.count).to eq(2) # group_stage_1 and project_stage_1 has the same hash
+ end
+
+ it 'runs without problem without stages' do
+ expect { migrate! }.not_to raise_error
+ end
+
+ context 'when invalid event identifier is discovered' do
+ it 'removes the stage' do
+ group_stage_1
+ invalid_group_stage
+
+ expect { migrate! }.not_to change { group_stage_1 }
+
+ expect(group_stages.find_by_id(invalid_group_stage.id)).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
new file mode 100644
index 00000000000..aa857cfdb70
--- /dev/null
+++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_context 'runners resolver setup' do
+ let_it_be(:user) { create_default(:user, :admin) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:subgroup) { create(:group, :public, parent: group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+
+ let_it_be(:inactive_project_runner) do
+ create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
+ end
+
+ let_it_be(:offline_project_runner) do
+ create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
+ end
+
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) }
+ let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: 'mnopqr', description: 'subgroup runner', contacted_at: 1.second.ago) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
+end