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 3e38f6eebe7..00000000000 Binary files a/doc/user/application_security/img/vulnerability-check_v13_4.png and /dev/null 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 Binary files /dev/null and b/doc/user/application_security/img/vulnerability-check_v14_2.png 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" } - 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