From d67a86595f0866927815e0945bff032a35c76da9 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 23 Aug 2021 12:10:04 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- GITALY_SERVER_VERSION | 2 +- app/controllers/groups/runners_controller.rb | 2 +- .../groups/settings/ci_cd_controller.rb | 2 +- app/finders/ci/runners_finder.rb | 18 +- .../resolvers/ci/group_runners_resolver.rb | 26 +++ app/graphql/resolvers/ci/runners_resolver.rb | 15 +- .../types/ci/runner_membership_filter_enum.rb | 18 ++ app/graphql/types/group_type.rb | 6 + app/helpers/ci/pipeline_editor_helper.rb | 1 - ...yml => create_vulnerabilities_via_api.yml} | 12 +- .../dast_runner_site_validation.yml | 8 - ...0210731132939_backfill_stage_event_hash.rb | 115 ++++++++++ db/schema_migrations/20210731132939 | 1 + db/structure.sql | 6 +- doc/api/graphql/reference/index.md | 103 ++++++++- doc/user/application_security/dast/index.md | 6 +- .../img/vulnerability-check_v13_4.png | Bin 25832 -> 0 bytes .../img/vulnerability-check_v14_2.png | Bin 0 -> 23147 bytes doc/user/application_security/index.md | 18 +- doc/user/infrastructure/index.md | 54 ++++- locale/gitlab.pot | 10 +- qa/qa/resource/issue.rb | 7 +- qa/qa/runtime/search.rb | 6 +- .../1_manage/import_large_github_repo_spec.rb | 39 ++-- spec/finders/ci/runners_finder_spec.rb | 126 +++++++---- .../ci/group_runners_resolver_spec.rb | 94 +++++++++ .../resolvers/ci/runners_resolver_spec.rb | 199 ++++-------------- .../helpers/ci/pipeline_editor_helper_spec.rb | 5 - .../backfill_stage_event_hash_spec.rb | 103 +++++++++ .../runners_resolver_shared_context.rb | 22 ++ 30 files changed, 757 insertions(+), 267 deletions(-) create mode 100644 app/graphql/resolvers/ci/group_runners_resolver.rb create mode 100644 app/graphql/types/ci/runner_membership_filter_enum.rb rename config/feature_flags/development/{dast_meta_tag_validation.yml => create_vulnerabilities_via_api.yml} (53%) delete mode 100644 config/feature_flags/development/dast_runner_site_validation.yml create mode 100644 db/post_migrate/20210731132939_backfill_stage_event_hash.rb create mode 100644 db/schema_migrations/20210731132939 delete mode 100644 doc/user/application_security/img/vulnerability-check_v13_4.png create mode 100644 doc/user/application_security/img/vulnerability-check_v14_2.png create mode 100644 spec/graphql/resolvers/ci/group_runners_resolver_spec.rb create mode 100644 spec/migrations/backfill_stage_event_hash_spec.rb create mode 100644 spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb 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 similarity index 53% rename from config/feature_flags/development/dast_meta_tag_validation.yml rename to 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` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `vulnerability` | [`Vulnerability`](#vulnerability) | The vulnerability after state change. | +### `Mutation.vulnerabilityCreate` + +Input type: `VulnerabilityCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `confidence` | [`VulnerabilityConfidence`](#vulnerabilityconfidence) | Confidence of the vulnerability (defaults to `unknown`). | +| `confirmedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to confirmed (defaults to creation time if status is `confirmed`). | +| `description` | [`String!`](#string) | Description of the vulnerability. | +| `detectedAt` | [`Time`](#time) | Timestamp of when the vulnerability was first detected (defaults to creation time). | +| `dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to dismissed (defaults to creation time if status is `dismissed`). | +| `identifiers` | [`[VulnerabilityIdentifierInput!]!`](#vulnerabilityidentifierinput) | Array of CVE or CWE identifiers for the vulnerability. | +| `message` | [`String`](#string) | Additional information about the vulnerability. | +| `project` | [`ProjectID!`](#projectid) | ID of the project to attach the vulnerability to. | +| `resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to resolved (defaults to creation time if status is `resolved`). | +| `scannerName` | [`String!`](#string) | Name of the security scanner used to discover the vulnerability. | +| `scannerType` | [`SecurityScannerType!`](#securityscannertype) | Type of the security scanner used to discover the vulnerability. | +| `severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability (defaults to `unknown`). | +| `solution` | [`String`](#string) | How to fix this vulnerability. | +| `state` | [`VulnerabilityState`](#vulnerabilitystate) | State of the vulnerability (defaults to `detected`). | +| `title` | [`String!`](#string) | Title of the vulnerability. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `vulnerability` | [`Vulnerability`](#vulnerability) | Vulnerability created. | + ### `Mutation.vulnerabilityDismiss` Input type: `VulnerabilityDismissInput` @@ -10004,6 +10037,27 @@ four standard [pagination arguments](#connection-pagination-arguments): | `search` | [`String`](#string) | Search project with most similar names or paths. | | `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 | +| ---- | ---- | ----------- | +| `membership` | [`RunnerMembershipFilter`](#runnermembershipfilter) | Control which runners to include in the results. | +| `search` | [`String`](#string) | Filter by full token or partial text in description field. | +| `sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. | +| `status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. | +| `tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). | +| `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. | `coverageFuzzing` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `coverage_fuzzing` scan. | | `dast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dast` scan. | | `dependencyScanning` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dependency_scanning` scan. | +| `generic` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `generic` scan. | | `sast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `sast` scan. | | `secretDetection` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `secret_detection` scan. | @@ -14141,7 +14196,7 @@ Represents a vulnerability. | `notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) | | `primaryIdentifier` | [`VulnerabilityIdentifier`](#vulnerabilityidentifier) | Primary identifier of the vulnerability. | | `project` | [`Project`](#project) | The project on which the vulnerability was found. | -| `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. | +| `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. | | `resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to resolved. | | `resolvedBy` | [`UserCore`](#usercore) | The user that resolved the vulnerability. | | `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. | `dependency` | [`VulnerableDependency`](#vulnerabledependency) | Dependency containing the vulnerability. | | `file` | [`String`](#string) | Path to the vulnerable file. | +### `VulnerabilityLocationGeneric` + +Represents the location of a vulnerability found by a generic scanner. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `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. | `MISSING` | Requirements without any test report. | | `PASSED` | | +### `RunnerMembershipFilter` + +Values for filtering runners in namespaces. + +| Value | Description | +| ----- | ----------- | +| `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). | +| `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. | `private` | The snippet is visible only to the snippet creator. | | `public` | The snippet can be accessed without any authentication. | +### `VulnerabilityConfidence` + +Confidence that a given vulnerability is present in the codebase. + +| Value | Description | +| ----- | ----------- | +| `CONFIRMED` | | +| `EXPERIMENTAL` | | +| `HIGH` | | +| `IGNORE` | | +| `LOW` | | +| `MEDIUM` | | +| `UNKNOWN` | | + ### `VulnerabilityDismissalReason` The dismissal reason of the Vulnerability. @@ -15933,6 +16021,7 @@ The type of the security scan that found the vulnerability. | `COVERAGE_FUZZING` | | | `DAST` | | | `DEPENDENCY_SCANNING` | | +| `GENERIC` | | | `SAST` | | | `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. | `width` | [`Int`](#int) | Total width of the image. | | `x` | [`Int`](#int) | X position of the note. | | `y` | [`Int`](#int) | Y position of the note. | + +### `VulnerabilityIdentifierInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `externalId` | [`String`](#string) | External ID of the vulnerability identifier. | +| `externalType` | [`String`](#string) | External type of the vulnerability identifier. | +| `name` | [`String!`](#string) | Name of the vulnerability identifier. | +| `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 3e38f6eebe76cb038f782f45fa103e586feca269..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25832 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�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 HY$E>)Cxjax literal 0 HcmV?d00001 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" } - expected = type == 'mr' ? mrs : gl_issues - actual = type == 'mr' ? gh_prs : gh_issues # 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}" - ) + 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 + 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 correct runner' do - expect(subject).to eq([runner_project_3]) + it 'returns runners belonging to group' do + expect(subject).to eq([runner_group]) end end - context 'by status' do - let(:params) { { status_status: 'paused' } } + context 'with unknown membership' do + let(:membership) { :unsupported } - it 'returns correct runner' do - expect(subject).to eq([runner_sub_group_1]) + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError, 'Invalid membership filter') end end - context 'by tag_name' do - let(:params) { { tag_name: %w[runner_tag] } } + context 'with nil group' do + let(:target_group) { nil } - it 'returns correct runner' do - expect(subject).to eq([runner_project_5]) + 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 runner type' do - let(:params) { { type_type: 'project_type' } } + context 'with sort param' do + let(:extra_params) { { sort: 'contacted_asc' } } - 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 '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 '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]) + 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(:obj) { nil } + let(:args) { {} } - let(:args) do - {} - end + subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) } - context 'when the user cannot see runners' do - let(:user) { create(:user) } + include_context 'runners resolver setup' + + # 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 no runners' do - is_expected.to be_empty + expect(subject.items.to_a).to eq([]) end end - context 'without sort' do + context 'when user can see runners' do + let(:obj) { nil } + it 'returns all the runners' do - is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner) + expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner) end end - context 'with a sort argument' do - context "set to :contacted_asc" do - let(:args) do - { sort: :contacted_asc } - end + # Then, we can check specific edge cases for this resolver + context 'with obj not set to nil' do + let(:obj) { build(:project) } - 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 - - 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) } + it 'raises an error' do + expect { subject }.to raise_error(a_string_including('Unexpected parent type')) end end - context 'when type 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 - { type: runner_type.to_s } + { + status: 'active', + type: :instance_type, + tag_list: ['active_runner'], + search: 'abc', + sort: :contacted_asc + } end - context 'to instance runners' do - let(:runner_type) { :instance_type } - - it 'returns the instance runner' do - is_expected.to contain_exactly(instance_runner) - 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 - context 'to group runners' do - let(:runner_type) { :group_type } + 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]) - 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 - 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 } - - it 'returns the offline project runner' do - is_expected.to contain_exactly(offline_project_runner) - end - 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'] } - - 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 - end - end - - context 'when text is filtered' do - 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 + 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