From 62c78157be8fe8888787162293f13945a5fa5d3e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Dec 2021 12:10:29 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../ide/stores/modules/pipelines/actions.js | 8 +- app/mailers/previews/notify_preview.rb | 2 +- app/models/clusters/applications/runner.rb | 2 +- app/models/internal_id.rb | 2 +- .../namespaces/traversal/linear_scopes.rb | 25 +- app/services/auto_merge/base_service.rb | 6 +- app/services/ci/register_runner_service.rb | 36 ++ .../deployments/update_environment_service.rb | 2 +- ...create_project_from_remote_file_service.rb | 21 +- doc/api/graphql/reference/index.md | 134 ++++++ doc/api/protected_branches.md | 12 +- doc/api/vulnerabilities.md | 184 +++++++- doc/ci/yaml/index.md | 6 +- lib/api/ci/helpers/runner.rb | 8 - lib/api/ci/runner.rb | 17 +- lib/gitlab/database/grant.rb | 2 +- lib/gitlab/database/load_balancing/setup.rb | 4 +- .../stores/modules/pipelines/actions_spec.js | 18 + spec/lib/gitlab/import_export/all_models.yml | 1 + .../metrics/samplers/database_sampler_spec.rb | 4 +- spec/models/application_record_spec.rb | 10 +- spec/models/internal_id_spec.rb | 6 +- .../api/ci/runner/runners_post_spec.rb | 423 +++--------------- .../ci/register_runner_service_spec.rb | 226 ++++++++++ ...e_project_from_remote_file_service_spec.rb | 49 +- .../namespaces/traversal_scope_examples.rb | 19 + .../database/multiple_databases_spec.rb | 4 +- 27 files changed, 798 insertions(+), 433 deletions(-) create mode 100644 app/services/ci/register_runner_service.rb create mode 100644 spec/services/ci/register_runner_service_spec.rb diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 9cf8d5a360e..51872993f16 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -53,9 +53,15 @@ export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipeline commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline); }; -export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { +export const fetchLatestPipeline = ({ commit, dispatch, rootGetters }) => { if (eTagPoll) return; + if (!rootGetters.lastCommit) { + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); + dispatch('stopPipelinePolling'); + return; + } + dispatch('requestLatestPipeline'); eTagPoll = new Poll({ diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 13b24e099c9..8e30eeee73f 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -246,7 +246,7 @@ class NotifyPreview < ActionMailer::Preview def cleanup email = nil - ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases + ApplicationRecord.transaction do email = yield raise ActiveRecord::Rollback end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index b57a24dead0..5db065bad7f 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.35.0' + VERSION = '0.36.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 10d24ab50b2..b502d5e354d 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -57,7 +57,7 @@ class InternalId < ApplicationRecord self.internal_id_transactions_total.increment( operation: operation, usage: usage.to_s, - in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s # rubocop: disable Database/MultipleDatabases + in_transaction: InternalId.connection.transaction_open?.to_s ) end diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb index 0dfb7320461..3ed525f34c7 100644 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ b/app/models/namespaces/traversal/linear_scopes.rb @@ -25,15 +25,20 @@ module Namespaces def self_and_ancestors(include_self: true, hierarchy_order: nil) return super unless use_traversal_ids_for_ancestor_scopes? + ancestors_cte, base_cte = ancestor_ctes + namespaces = Arel::Table.new(:namespaces) + records = unscoped - .where(id: select('unnest(traversal_ids)')) + .with(base_cte.to_arel, ancestors_cte.to_arel) + .distinct + .from([ancestors_cte.table, namespaces]) + .where(namespaces[:id].eq(ancestors_cte.table[:ancestor_id])) .order_by_depth(hierarchy_order) - .normal_select if include_self records else - records.where.not(id: all.as_ids) + records.where(ancestors_cte.table[:base_id].not_eq(ancestors_cte.table[:ancestor_id])) end end @@ -150,6 +155,20 @@ module Namespaces records.where('namespaces.id <> base.id') end end + + def ancestor_ctes + base_scope = all.select('namespaces.id', 'namespaces.traversal_ids') + base_cte = Gitlab::SQL::CTE.new(:base_ancestors_cte, base_scope) + + # We have to alias id with 'AS' to avoid ambiguous column references by calling methods. + ancestors_scope = unscoped + .unscope(where: [:type]) + .select('id as base_id', 'unnest(traversal_ids) as ancestor_id') + .from(base_cte.table) + ancestors_cte = Gitlab::SQL::CTE.new(:ancestors_cte, ancestors_scope) + + [ancestors_cte, base_cte] + end end end end diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index da80211f9bb..4ed4368d3b7 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -6,7 +6,7 @@ module AutoMerge include MergeRequests::AssignsMergeParams def execute(merge_request) - ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases + ApplicationRecord.transaction do register_auto_merge_parameters!(merge_request) yield if block_given? end @@ -29,7 +29,7 @@ module AutoMerge end def cancel(merge_request) - ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases + ApplicationRecord.transaction do clear_auto_merge_parameters!(merge_request) yield if block_given? end @@ -41,7 +41,7 @@ module AutoMerge end def abort(merge_request, reason) - ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases + ApplicationRecord.transaction do clear_auto_merge_parameters!(merge_request) yield if block_given? end diff --git a/app/services/ci/register_runner_service.rb b/app/services/ci/register_runner_service.rb new file mode 100644 index 00000000000..0a2027e33ce --- /dev/null +++ b/app/services/ci/register_runner_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Ci + class RegisterRunnerService + def execute(registration_token, attributes) + runner_type_attrs = check_token_and_extract_attrs(registration_token) + + return unless runner_type_attrs + + ::Ci::Runner.create(attributes.merge(runner_type_attrs)) + end + + private + + def check_token_and_extract_attrs(registration_token) + if runner_registration_token_valid?(registration_token) + # Create shared runner. Requires admin access + { runner_type: :instance_type } + elsif runner_registrar_valid?('project') && project = ::Project.find_by_runners_token(registration_token) + # Create a specific runner for the project + { runner_type: :project_type, projects: [project] } + elsif runner_registrar_valid?('group') && group = ::Group.find_by_runners_token(registration_token) + # Create a specific runner for the group + { runner_type: :group_type, groups: [group] } + end + end + + def runner_registration_token_valid?(registration_token) + ActiveSupport::SecurityUtils.secure_compare(registration_token, Gitlab::CurrentSettings.runners_registration_token) + end + + def runner_registrar_valid?(type) + Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type) + end + end +end diff --git a/app/services/deployments/update_environment_service.rb b/app/services/deployments/update_environment_service.rb index 83c37257297..19b950044d0 100644 --- a/app/services/deployments/update_environment_service.rb +++ b/app/services/deployments/update_environment_service.rb @@ -26,7 +26,7 @@ module Deployments end def update_environment(deployment) - ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases + ApplicationRecord.transaction do # Renew attributes at update renew_external_url renew_auto_stop_in diff --git a/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb b/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb index bbfdaf692f9..edb9dc8ad91 100644 --- a/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb +++ b/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb @@ -4,7 +4,10 @@ module Import module GitlabProjects class CreateProjectFromRemoteFileService < CreateProjectFromUploadedFileService FILE_SIZE_LIMIT = 10.gigabytes - ALLOWED_CONTENT_TYPES = ['application/gzip'].freeze + ALLOWED_CONTENT_TYPES = [ + 'application/gzip', # most common content-type when fetching a tar.gz + 'application/x-tar' # aws-s3 uses x-tar for tar.gz files + ].freeze validate :valid_remote_import_url? validate :validate_file_size @@ -44,17 +47,27 @@ module Import end def validate_content_type + # AWS-S3 presigned URLs don't respond to HTTP HEAD requests, + # so file type cannot be validated + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75170#note_748059103 + return if amazon_s3? + if headers['content-type'].blank? errors.add(:base, "Missing 'ContentType' header") elsif !ALLOWED_CONTENT_TYPES.include?(headers['content-type']) errors.add(:base, "Remote file content type '%{content_type}' not allowed. (Allowed content types: %{allowed})" % { content_type: headers['content-type'], - allowed: ALLOWED_CONTENT_TYPES.join(',') + allowed: ALLOWED_CONTENT_TYPES.join(', ') }) end end def validate_file_size + # AWS-S3 presigned URLs don't respond to HTTP HEAD requests, + # so file size cannot be validated + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75170#note_748059103 + return if amazon_s3? + if headers['content-length'].to_i == 0 errors.add(:base, "Missing 'ContentLength' header") elsif headers['content-length'].to_i > FILE_SIZE_LIMIT @@ -64,6 +77,10 @@ module Import end end + def amazon_s3? + headers['Server'] == 'AmazonS3' && headers['x-amz-request-id'].present? + end + def headers return {} if params[:remote_import_url].blank? || !valid_remote_import_url? diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 123e65162d8..573659881e6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7439,6 +7439,29 @@ The edge type for [`ScanExecutionPolicy`](#scanexecutionpolicy). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`ScanExecutionPolicy`](#scanexecutionpolicy) | The item at the end of the edge. | +#### `ScanResultPolicyConnection` + +The connection type for [`ScanResultPolicy`](#scanresultpolicy). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[ScanResultPolicyEdge]`](#scanresultpolicyedge) | A list of edges. | +| `nodes` | [`[ScanResultPolicy]`](#scanresultpolicy) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `ScanResultPolicyEdge` + +The edge type for [`ScanResultPolicy`](#scanresultpolicy). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`ScanResultPolicy`](#scanresultpolicy) | The item at the end of the edge. | + #### `ScannedResourceConnection` The connection type for [`ScannedResource`](#scannedresource). @@ -7741,6 +7764,29 @@ The edge type for [`TestSuiteSummary`](#testsuitesummary). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`TestSuiteSummary`](#testsuitesummary) | The item at the end of the edge. | +#### `TimelineEventTypeConnection` + +The connection type for [`TimelineEventType`](#timelineeventtype). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[TimelineEventTypeEdge]`](#timelineeventtypeedge) | A list of edges. | +| `nodes` | [`[TimelineEventType]`](#timelineeventtype) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `TimelineEventTypeEdge` + +The edge type for [`TimelineEventType`](#timelineeventtype). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`TimelineEventType`](#timelineeventtype) | The item at the end of the edge. | + #### `TimelogConnection` The connection type for [`Timelog`](#timelog). @@ -12989,6 +13035,7 @@ Represents vulnerability finding of a security report on the pipeline. | `requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request member access to the project. | | `requirementStatesCount` | [`RequirementStatesCount`](#requirementstatescount) | Number of requirements for the project by their state. | | `sastCiConfiguration` | [`SastCiConfiguration`](#sastciconfiguration) | SAST CI configuration for the project. | +| `scanResultPolicies` | [`ScanResultPolicyConnection`](#scanresultpolicyconnection) | Scan Result Policies of the project. (see [Connections](#connections)) | | `securityDashboardPath` | [`String`](#string) | Path to project's security dashboard. | | `securityScanners` | [`SecurityScanners`](#securityscanners) | Information about security analyzers used in the project. | | `sentryErrors` | [`SentryErrorCollection`](#sentryerrorcollection) | Paginated collection of Sentry errors on the project. | @@ -13295,6 +13342,35 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | `iids` | [`[ID!]`](#id) | IIDs of on-call schedules. | +##### `Project.incidentManagementTimelineEvent` + +Incident Management Timeline event associated with the incident. + +Returns [`TimelineEventType`](#timelineeventtype). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event. | +| `incidentId` | [`IssueID!`](#issueid) | ID of the incident. | + +##### `Project.incidentManagementTimelineEvents` + +Incident Management Timeline events associated with the incident. + +Returns [`TimelineEventTypeConnection`](#timelineeventtypeconnection). + +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 | +| ---- | ---- | ----------- | +| `incidentId` | [`IssueID!`](#issueid) | ID of the incident. | + ##### `Project.issue` A single issue of the project. @@ -14429,6 +14505,20 @@ Represents the scan execution policy. | `updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. | | `yaml` | [`String!`](#string) | YAML definition of the policy. | +### `ScanResultPolicy` + +Represents the scan result policy. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `description` | [`String!`](#string) | Description of the policy. | +| `enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. | +| `name` | [`String!`](#string) | Name of the policy. | +| `updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. | +| `yaml` | [`String!`](#string) | YAML definition of the policy. | + ### `ScannedResource` Represents a resource scanned by a security scan. @@ -14999,6 +15089,27 @@ Represents a historically accurate report about the timebox. | `burnupTimeSeries` | [`[BurnupChartDailyTotals!]`](#burnupchartdailytotals) | Daily scope and completed totals for burnup charts. | | `stats` | [`TimeReportStats`](#timereportstats) | Represents the time report stats for the timebox. | +### `TimelineEventType` + +Describes an incident management timeline event. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `action` | [`String!`](#string) | Indicates the timeline event icon. | +| `author` | [`UserCore`](#usercore) | User that created the timeline event. | +| `createdAt` | [`Time!`](#time) | Timestamp when the event created. | +| `editable` | [`Boolean!`](#boolean) | Indicates the timeline event is editable. | +| `id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event. | +| `incident` | [`Issue!`](#issue) | Incident of the timeline event. | +| `note` | [`String`](#string) | Text note of the timeline event. | +| `noteHtml` | [`String`](#string) | HTML note of the timeline event. | +| `occurredAt` | [`Time!`](#time) | Timestamp when the event occurred. | +| `promotedFromNote` | [`Note`](#note) | Note from which the timeline event was created. | +| `updatedAt` | [`Time!`](#time) | Timestamp when the event updated. | +| `updatedByUser` | [`UserCore`](#usercore) | User that updated the timeline event. | + ### `Timelog` #### Fields @@ -17691,6 +17802,12 @@ A `IncidentManagementOncallRotationID` is a global ID. It is encoded as a string An example `IncidentManagementOncallRotationID` is: `"gid://gitlab/IncidentManagement::OncallRotation/1"`. +### `IncidentManagementTimelineEventID` + +A `IncidentManagementTimelineEventID` is a global ID. It is encoded as a string. + +An example `IncidentManagementTimelineEventID` is: `"gid://gitlab/IncidentManagement::TimelineEvent/1"`. + ### `Int` Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. @@ -18180,6 +18297,23 @@ Implementations: | `discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) | | `notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) | +#### `OrchestrationPolicy` + +Implementations: + +- [`ScanExecutionPolicy`](#scanexecutionpolicy) +- [`ScanResultPolicy`](#scanresultpolicy) + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `description` | [`String!`](#string) | Description of the policy. | +| `enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. | +| `name` | [`String!`](#string) | Name of the policy. | +| `updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. | +| `yaml` | [`String!`](#string) | YAML definition of the policy. | + #### `PackageFileMetadata` Represents metadata associated with a Package file. diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index d17341759ad..e9bad8e4f2d 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -201,13 +201,13 @@ curl --request POST --header "PRIVATE-TOKEN: " "https://gitla | -------------------------------------------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | yes | The name of the branch or wildcard | -| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) | -| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) | -| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) | +| `push_access_level` | integer | no | Access levels allowed to push (defaults: `40`, Maintainer role) | +| `merge_access_level` | integer | no | Access levels allowed to merge (defaults: `40`, Maintainer role) | +| `unprotect_access_level` | integer | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) | | `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) | -| `allowed_to_push` **(PREMIUM)** | array | no | Array of access levels allowed to push, with each described by a hash | -| `allowed_to_merge` **(PREMIUM)** | array | no | Array of access levels allowed to merge, with each described by a hash | -| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of access levels allowed to unprotect, with each described by a hash | +| `allowed_to_push` **(PREMIUM)** | array | no | Array of access levels allowed to push, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}` | +| `allowed_to_merge` **(PREMIUM)** | array | no | Array of access levels allowed to merge, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}` | +| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of access levels allowed to unprotect, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}` | | `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) | Example response: diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md index 1c6f7a760e6..acaf848f656 100644 --- a/doc/api/vulnerabilities.md +++ b/doc/api/vulnerabilities.md @@ -19,7 +19,7 @@ This API is in the process of being deprecated and considered unstable. The response payload may be subject to change or breakage across GitLab releases. Please use the [GraphQL API](graphql/reference/index.md#queryvulnerabilities) -instead. +instead. See the [GraphQL examples](#replace-rest-with-graphql) to get started. Every API call to vulnerabilities must be [authenticated](index.md#authentication). @@ -272,3 +272,185 @@ Example response: "closed_at": null } ``` + +## Replace REST with GraphQL + +To prepare for the [upcoming deprecation](https://gitlab.com/groups/gitlab-org/-/epics/5118) of +this REST API endpoint, use the examples below to learn how to perform the equivalent operations +using the GraphQL API. + +### GraphQL - Single vulnerability + +Use [`Query.vulnerability`](graphql/reference/#queryvulnerability). + +```graphql +{ + vulnerability(id: "gid://gitlab/Vulnerability/20345379") { + title + description + state + severity + reportType + project { + id + name + fullPath + } + detectedAt + confirmedAt + resolvedAt + resolvedBy { + id + username + } + } +} +``` + +Example response: + +```json +{ + "data": { + "vulnerability": { + "title": "Improper Input Validation in railties", + "description": "A remote code execution vulnerability in development mode Rails beta3 can allow an attacker to guess the automatically generated development mode secret token. This secret token can be used in combination with other Rails internals to escalate to a remote code execution exploit.", + "state": "RESOLVED", + "severity": "CRITICAL", + "reportType": "DEPENDENCY_SCANNING", + "project": { + "id": "gid://gitlab/Project/6102100", + "name": "security-reports", + "fullPath": "gitlab-examples/security/security-reports" + }, + "detectedAt": "2021-10-14T03:13:41Z", + "confirmedAt": "2021-12-14T01:45:56Z", + "resolvedAt": "2021-12-14T01:45:59Z", + "resolvedBy": { + "id": "gid://gitlab/User/480804", + "username": "thiagocsf" + } + } + } +} +``` + +### GraphQL - Confirm vulnerability + +Use [`Mutation.vulnerabilityConfirm`](graphql/reference/#mutationvulnerabilityconfirm). + +```graphql +mutation { + vulnerabilityConfirm(input: { id: "gid://gitlab/Vulnerability/23577695"}) { + vulnerability { + state + } + errors + } +} +``` + +Example response: + +```json +{ + "data": { + "vulnerabilityConfirm": { + "vulnerability": { + "state": "CONFIRMED" + }, + "errors": [] + } + } +} +``` + +### GraphQL - Resolve vulnerability + +Use [`Mutation.vulnerabilityResolve`](graphql/reference/#mutationvulnerabilityresolve). + +```graphql +mutation { + vulnerabilityResolve(input: { id: "gid://gitlab/Vulnerability/23577695"}) { + vulnerability { + state + } + errors + } +} +``` + +Example response: + +```json +{ + "data": { + "vulnerabilityConfirm": { + "vulnerability": { + "state": "RESOLVED" + }, + "errors": [] + } + } +} +``` + +### GraphQL - Dismiss vulnerability + +Use [`Mutation.vulnerabilityDismiss`](graphql/reference/#mutationvulnerabilitydismiss). + +```graphql +mutation { + vulnerabilityDismiss(input: { id: "gid://gitlab/Vulnerability/23577695"}) { + vulnerability { + state + } + errors + } +} +``` + +Example response: + +```json +{ + "data": { + "vulnerabilityConfirm": { + "vulnerability": { + "state": "DISMISSED" + }, + "errors": [] + } + } +} +``` + +### GraphQL - Revert vulnerability to detected state + +Use [`Mutation.vulnerabilityRevertToDetected`](graphql/reference/#mutationvulnerabilityreverttodetected). + +```graphql +mutation { + vulnerabilityRevertToDetected(input: { id: "gid://gitlab/Vulnerability/20345379"}) { + vulnerability { + state + } + errors + } +} +``` + +Example response: + +```json +{ + "data": { + "vulnerabilityConfirm": { + "vulnerability": { + "state": "DETECTED" + }, + "errors": [] + } + } +} +``` diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index ed05ef08d02..11765ba4ac6 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3066,8 +3066,10 @@ job: - If a rule matches and has no `when` defined, the rule uses the `when` defined for the job, which defaults to `on_success` if not defined. -- You can define `when` once per rule, or once at the job-level, which applies to - all rules. You can't mix `when` at the job-level with `when` in rules. +- In GitLab 14.5 and earlier, you can define `when` once per rule, or once at the job-level, + which applies to all rules. You can't mix `when` at the job-level with `when` in rules. +- In GitLab 14.6 and later, you can [mix `when` at the job-level with `when` in rules](https://gitlab.com/gitlab-org/gitlab/-/issues/219437). + `when` configuration in `rules` takes precedence over `when` at the job-level. - Unlike variables in [`script`](../variables/index.md#use-cicd-variables-in-job-scripts) sections, variables in rules expressions are always formatted as `$VARIABLE`. - You can use `rules:if` with `include` to [conditionally include other configuration files](includes.md#use-rules-with-include). diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb index 72c388160b4..43ed35b99fd 100644 --- a/lib/api/ci/helpers/runner.rb +++ b/lib/api/ci/helpers/runner.rb @@ -11,14 +11,6 @@ module API JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN' JOB_TOKEN_PARAM = :token - def runner_registration_token_valid? - ActiveSupport::SecurityUtils.secure_compare(params[:token], Gitlab::CurrentSettings.runners_registration_token) - end - - def runner_registrar_valid?(type) - Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type) - end - def authenticate_runner! forbidden! unless current_runner diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 4317789f7aa..5b996433584 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -28,21 +28,8 @@ module API attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout]) .merge(get_runner_details_from_request) - attributes = - if runner_registration_token_valid? - # Create shared runner. Requires admin access - attributes.merge(runner_type: :instance_type) - elsif runner_registrar_valid?('project') && @project = Project.find_by_runners_token(params[:token]) - # Create a specific runner for the project - attributes.merge(runner_type: :project_type, projects: [@project]) - elsif runner_registrar_valid?('group') && @group = Group.find_by_runners_token(params[:token]) - # Create a specific runner for the group - attributes.merge(runner_type: :group_type, groups: [@group]) - else - forbidden! - end - - @runner = ::Ci::Runner.create(attributes) + @runner = ::Ci::RegisterRunnerService.new.execute(params[:token], attributes) + forbidden! unless @runner if @runner.persisted? present @runner, with: Entities::Ci::RunnerRegistrationDetails diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb index c8a30c68bc6..0093848ee6f 100644 --- a/lib/gitlab/database/grant.rb +++ b/lib/gitlab/database/grant.rb @@ -10,7 +10,7 @@ module Gitlab # We _must not_ use quote_table_name as this will produce double # quotes on PostgreSQL and for "has_table_privilege" we need single # quotes. - connection = ActiveRecord::Base.connection # rubocop: disable Database/MultipleDatabases + connection = ApplicationRecord.connection quoted_table = connection.quote(table) begin diff --git a/lib/gitlab/database/load_balancing/setup.rb b/lib/gitlab/database/load_balancing/setup.rb index ef38f42f50b..126c8bb2aa6 100644 --- a/lib/gitlab/database/load_balancing/setup.rb +++ b/lib/gitlab/database/load_balancing/setup.rb @@ -104,11 +104,9 @@ module Gitlab end end - # rubocop:disable Database/MultipleDatabases def connection - use_model_load_balancing? ? super : ActiveRecord::Base.connection + use_model_load_balancing? ? super : ApplicationRecord.connection end - # rubocop:enable Database/MultipleDatabases end end end diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js index 9aa31136c89..3ede37e2eed 100644 --- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js +++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js @@ -188,6 +188,24 @@ describe('IDE pipelines actions', () => { .catch(done.fail); }); }); + + it('sets latest pipeline to `null` and stops polling on empty project', (done) => { + mockedState = { + ...mockedState, + rootGetters: { + lastCommit: null, + }, + }; + + testAction( + fetchLatestPipeline, + {}, + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }], + [{ type: 'stopPipelinePolling' }], + done, + ); + }); }); describe('requestJobs', () => { diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7ed80cbcf66..fb7eb4668b9 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -58,6 +58,7 @@ issues: - test_reports - requirement - incident_management_issuable_escalation_status +- incident_management_timeline_events - pending_escalations - customer_relations_contacts - issue_customer_relations_contacts diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb index e8f8947c9e8..c88d8c17eac 100644 --- a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do end context 'when replica hosts are configured' do - let(:main_load_balancer) { ActiveRecord::Base.load_balancer } # rubocop:disable Database/MultipleDatabases + let(:main_load_balancer) { ApplicationRecord.load_balancer } let(:main_replica_host) { main_load_balancer.host } let(:ci_load_balancer) { double(:load_balancer, host_list: ci_host_list, configuration: configuration) } @@ -117,7 +117,7 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do end context 'when the base model has replica connections' do - let(:main_load_balancer) { ActiveRecord::Base.load_balancer } # rubocop:disable Database/MultipleDatabases + let(:main_load_balancer) { ApplicationRecord.load_balancer } let(:main_replica_host) { main_load_balancer.host } let(:ci_load_balancer) { double(:load_balancer, host_list: ci_host_list, configuration: configuration) } diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index f0212da3041..9c9a048999c 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -147,22 +147,20 @@ RSpec.describe ApplicationRecord do end end - # rubocop:disable Database/MultipleDatabases it 'increments a counter when a transaction is created in ActiveRecord' do expect(described_class.connection.transaction_open?).to be false expect(::Gitlab::Database::Metrics) .to receive(:subtransactions_increment) - .with('ActiveRecord::Base') + .with('ApplicationRecord') .once - ActiveRecord::Base.transaction do - ActiveRecord::Base.transaction(requires_new: true) do - expect(ActiveRecord::Base.connection.transaction_open?).to be true + ApplicationRecord.transaction do + ApplicationRecord.transaction(requires_new: true) do + expect(ApplicationRecord.connection.transaction_open?).to be true end end end - # rubocop:enable Database/MultipleDatabases end describe '.with_fast_read_statement_timeout' do diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 51b27151ba2..f0007e1203c 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -87,7 +87,7 @@ RSpec.describe InternalId do context 'when executed outside of transaction' do it 'increments counter with in_transaction: "false"' do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } # rubocop: disable Database/MultipleDatabases + allow(ApplicationRecord.connection).to receive(:transaction_open?) { false } expect(InternalId.internal_id_transactions_total).to receive(:increment) .with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original @@ -146,7 +146,7 @@ RSpec.describe InternalId do let(:value) { 2 } it 'increments counter with in_transaction: "false"' do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } # rubocop: disable Database/MultipleDatabases + allow(ApplicationRecord.connection).to receive(:transaction_open?) { false } expect(InternalId.internal_id_transactions_total).to receive(:increment) .with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original @@ -217,7 +217,7 @@ RSpec.describe InternalId do context 'when executed outside of transaction' do it 'increments counter with in_transaction: "false"' do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } # rubocop: disable Database/MultipleDatabases + allow(ApplicationRecord.connection).to receive(:transaction_open?) { false } expect(InternalId.internal_id_transactions_total).to receive(:increment) .with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index a51d8b458f8..1a7dd6d76cd 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -3,21 +3,6 @@ require 'spec_helper' RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do - include StubGitlabCalls - include RedisHelpers - include WorkhorseHelpers - - let(:registration_token) { 'abcdefg123456' } - - before do - stub_feature_flags(ci_enable_live_trace: true) - stub_feature_flags(runner_registration_control: false) - stub_gitlab_calls - stub_application_setting(runners_registration_token: registration_token) - stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES) - allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) - end - describe '/api/v4/runners' do describe 'POST /api/v4/runners' do context 'when no token is provided' do @@ -30,380 +15,106 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when invalid token is provided' do it 'returns 403 error' do + allow_next_instance_of(::Ci::RegisterRunnerService) do |service| + allow(service).to receive(:execute).and_return(nil) + end + post api('/runners'), params: { token: 'invalid' } expect(response).to have_gitlab_http_status(:forbidden) end end - context 'when valid token is provided' do + context 'when valid parameters are provided' do def request - post api('/runners'), params: { token: token } + post api('/runners'), params: { + token: 'valid token', + description: 'server.hostname', + run_untagged: false, + tag_list: 'tag1, tag2', + locked: true, + active: true, + access_level: 'ref_protected', + maximum_timeout: 9000 + } end - context 'with a registration token' do - let(:token) { registration_token } + let_it_be(:new_runner) { create(:ci_runner) } - it 'creates runner with default values' do - request + before do + allow_next_instance_of(::Ci::RegisterRunnerService) do |service| + expected_params = { + description: 'server.hostname', + run_untagged: false, + tag_list: %w(tag1 tag2), + locked: true, + active: true, + access_level: 'ref_protected', + maximum_timeout: 9000 + }.stringify_keys - runner = ::Ci::Runner.first - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['id']).to eq(runner.id) - expect(json_response['token']).to eq(runner.token) - expect(runner.run_untagged).to be true - expect(runner.active).to be true - expect(runner.token).not_to eq(registration_token) - expect(runner).to be_instance_type - end - - it_behaves_like 'storing arguments in the application context for the API' do - subject { request } - - let(:expected_params) { { client_id: "runner/#{::Ci::Runner.first.id}" } } - end - - it_behaves_like 'not executing any extra queries for the application context' do - let(:subject_proc) { proc { request } } + allow(service).to receive(:execute) + .once + .with('valid token', a_hash_including(expected_params)) + .and_return(new_runner) end end - context 'when project token is used' do - let(:project) { create(:project) } - let(:token) { project.runners_token } + it 'creates runner' do + request - it 'creates project runner' do - request + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(new_runner.id) + expect(json_response['token']).to eq(new_runner.token) + end - expect(response).to have_gitlab_http_status(:created) - expect(project.runners.size).to eq(1) - runner = ::Ci::Runner.first - expect(runner.token).not_to eq(registration_token) - expect(runner.token).not_to eq(project.runners_token) - expect(runner).to be_project_type - end + it_behaves_like 'storing arguments in the application context for the API' do + subject { request } - it_behaves_like 'storing arguments in the application context for the API' do - subject { request } + let(:expected_params) { { client_id: "runner/#{new_runner.id}" } } + end - let(:expected_params) { { project: project.full_path, client_id: "runner/#{::Ci::Runner.first.id}" } } - end + it_behaves_like 'not executing any extra queries for the application context' do + let(:subject_proc) { proc { request } } + end + end - it_behaves_like 'not executing any extra queries for the application context' do - let(:subject_proc) { proc { request } } - end + context 'calling actual register service' do + include StubGitlabCalls - context 'when it exceeds the application limits' do - before do - create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago) - create(:plan_limits, :default_plan, ci_registered_project_runners: 1) - end + let(:registration_token) { 'abcdefg123456' } - it 'does not create runner' do - request + before do + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + end - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include('runner_projects.base' => ['Maximum number of ci registered project runners (1) exceeded']) - expect(project.runners.reload.size).to eq(1) - end - end + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' info is present" do + let(:value) { "#{param}_value" } - context 'when abandoned runners cause application limits to not be exceeded' do - before do - create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago) - create(:plan_limits, :default_plan, ci_registered_project_runners: 1) - end - - it 'creates runner' do - request + it "updates provided Runner's parameter" do + post api('/runners'), params: { + token: registration_token, + info: { param => value } + } expect(response).to have_gitlab_http_status(:created) - expect(json_response['message']).to be_nil - expect(project.runners.reload.size).to eq(2) - expect(project.runners.recent.size).to eq(1) - end - end - - context 'when valid runner registrars do not include project' do - before do - stub_application_setting(valid_runner_registrars: ['group']) - end - - context 'when feature flag is enabled' do - before do - stub_feature_flags(runner_registration_control: true) - end - - it 'returns 403 error' do - request - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when feature flag is disabled' do - it 'registers the runner' do - request - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.active).to be true - end + expect(::Ci::Runner.last.read_attribute(param.to_sym)).to eq(value) end end end - context 'when group token is used' do - let(:group) { create(:group) } - let(:token) { group.runners_token } - - it 'creates a group runner' do - request - - expect(response).to have_gitlab_http_status(:created) - expect(group.runners.reload.size).to eq(1) - runner = ::Ci::Runner.first - expect(runner.token).not_to eq(registration_token) - expect(runner.token).not_to eq(group.runners_token) - expect(runner).to be_group_type - end - - it_behaves_like 'storing arguments in the application context for the API' do - subject { request } - - let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{::Ci::Runner.first.id}" } } - end - - it_behaves_like 'not executing any extra queries for the application context' do - let(:subject_proc) { proc { request } } - end - - context 'when it exceeds the application limits' do - before do - create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago) - create(:plan_limits, :default_plan, ci_registered_group_runners: 1) - end - - it 'does not create runner' do - request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include('runner_namespaces.base' => ['Maximum number of ci registered group runners (1) exceeded']) - expect(group.runners.reload.size).to eq(1) - end - end - - context 'when abandoned runners cause application limits to not be exceeded' do - before do - create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago) - create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago) - create(:plan_limits, :default_plan, ci_registered_group_runners: 1) - end - - it 'creates runner' do - request - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['message']).to be_nil - expect(group.runners.reload.size).to eq(3) - expect(group.runners.recent.size).to eq(1) - end - end - - context 'when valid runner registrars do not include group' do - before do - stub_application_setting(valid_runner_registrars: ['project']) - end - - context 'when feature flag is enabled' do - before do - stub_feature_flags(runner_registration_control: true) - end - - it 'returns 403 error' do - request - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'when feature flag is disabled' do - it 'registers the runner' do - request - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.active).to be true - end - end - end - end - end - - context 'when runner description is provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - description: 'server.hostname' - } + it "sets the runner's ip_address" do + post api('/runners'), + params: { token: registration_token }, + headers: { 'X-Forwarded-For' => '123.111.123.111' } expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.description).to eq('server.hostname') + expect(::Ci::Runner.last.ip_address).to eq('123.111.123.111') end end - - context 'when runner tags are provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - tag_list: 'tag1, tag2' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) - end - end - - context 'when option for running untagged jobs is provided' do - context 'when tags are provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - run_untagged: false, - tag_list: ['tag'] - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.run_untagged).to be false - expect(::Ci::Runner.first.tag_list.sort).to eq(['tag']) - end - end - - context 'when tags are not provided' do - it 'returns 400 error' do - post api('/runners'), params: { - token: registration_token, - run_untagged: false - } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include( - 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs']) - end - end - end - - context 'when option for locking Runner is provided' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - locked: true - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.locked).to be true - end - end - - context 'when option for activating a Runner is provided' do - context 'when active is set to true' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - active: true - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.active).to be true - end - end - - context 'when active is set to false' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - active: false - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.active).to be false - end - end - end - - context 'when access_level is provided for Runner' do - context 'when access_level is set to ref_protected' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - access_level: 'ref_protected' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.ref_protected?).to be true - end - end - - context 'when access_level is set to not_protected' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - access_level: 'not_protected' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.ref_protected?).to be false - end - end - end - - context 'when maximum job timeout is specified' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - maximum_timeout: 9000 - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.maximum_timeout).to eq(9000) - end - - context 'when maximum job timeout is empty' do - it 'creates runner' do - post api('/runners'), params: { - token: registration_token, - maximum_timeout: '' - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.maximum_timeout).to be_nil - end - end - end - - %w(name version revision platform architecture).each do |param| - context "when info parameter '#{param}' info is present" do - let(:value) { "#{param}_value" } - - it "updates provided Runner's parameter" do - post api('/runners'), params: { - token: registration_token, - info: { param => value } - } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) - end - end - end - - it "sets the runner's ip_address" do - post api('/runners'), - params: { token: registration_token }, - headers: { 'X-Forwarded-For' => '123.111.123.111' } - - expect(response).to have_gitlab_http_status(:created) - expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111') - end end end end diff --git a/spec/services/ci/register_runner_service_spec.rb b/spec/services/ci/register_runner_service_spec.rb new file mode 100644 index 00000000000..e813a1d8b31 --- /dev/null +++ b/spec/services/ci/register_runner_service_spec.rb @@ -0,0 +1,226 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::RegisterRunnerService do + let(:registration_token) { 'abcdefg123456' } + + before do + stub_feature_flags(runner_registration_control: false) + stub_application_setting(runners_registration_token: registration_token) + stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES) + end + + describe '#execute' do + let(:token) { } + let(:args) { {} } + + subject { described_class.new.execute(token, args) } + + context 'when no token is provided' do + let(:token) { '' } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when invalid token is provided' do + let(:token) { 'invalid' } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when valid token is provided' do + context 'with a registration token' do + let(:token) { registration_token } + + it 'creates runner with default values' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.persisted?).to be_truthy + expect(subject.run_untagged).to be true + expect(subject.active).to be true + expect(subject.token).not_to eq(registration_token) + expect(subject).to be_instance_type + end + + context 'with non-default arguments' do + let(:args) do + { + description: 'some description', + active: false, + locked: true, + run_untagged: false, + tag_list: %w(tag1 tag2), + access_level: 'ref_protected', + maximum_timeout: 600, + name: 'some name', + version: 'some version', + revision: 'some revision', + platform: 'some platform', + architecture: 'some architecture', + ip_address: '10.0.0.1', + config: { + gpus: 'some gpu config' + } + } + end + + it 'creates runner with specified values', :aggregate_failures do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.active).to eq args[:active] + expect(subject.locked).to eq args[:locked] + expect(subject.run_untagged).to eq args[:run_untagged] + expect(subject.tags).to contain_exactly( + an_object_having_attributes(name: 'tag1'), + an_object_having_attributes(name: 'tag2') + ) + expect(subject.access_level).to eq args[:access_level] + expect(subject.maximum_timeout).to eq args[:maximum_timeout] + expect(subject.name).to eq args[:name] + expect(subject.version).to eq args[:version] + expect(subject.revision).to eq args[:revision] + expect(subject.platform).to eq args[:platform] + expect(subject.architecture).to eq args[:architecture] + expect(subject.ip_address).to eq args[:ip_address] + end + end + end + + context 'when project token is used' do + let(:project) { create(:project) } + let(:token) { project.runners_token } + + it 'creates project runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(project.runners.size).to eq(1) + is_expected.to eq(project.runners.first) + expect(subject.token).not_to eq(registration_token) + expect(subject.token).not_to eq(project.runners_token) + expect(subject).to be_project_type + end + + context 'when it exceeds the application limits' do + before do + create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago) + create(:plan_limits, :default_plan, ci_registered_project_runners: 1) + end + + it 'does not create runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.persisted?).to be_falsey + expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded']) + expect(project.runners.reload.size).to eq(1) + end + end + + context 'when abandoned runners cause application limits to not be exceeded' do + before do + create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago) + create(:plan_limits, :default_plan, ci_registered_project_runners: 1) + end + + it 'creates runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.errors).to be_empty + expect(project.runners.reload.size).to eq(2) + expect(project.runners.recent.size).to eq(1) + end + end + + context 'when valid runner registrars do not include project' do + before do + stub_application_setting(valid_runner_registrars: ['group']) + end + + context 'when feature flag is enabled' do + before do + stub_feature_flags(runner_registration_control: true) + end + + it 'returns 403 error' do + is_expected.to be_nil + end + end + + context 'when feature flag is disabled' do + it 'registers the runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.errors).to be_empty + expect(subject.active).to be true + end + end + end + end + + context 'when group token is used' do + let(:group) { create(:group) } + let(:token) { group.runners_token } + + it 'creates a group runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.errors).to be_empty + expect(group.runners.reload.size).to eq(1) + expect(subject.token).not_to eq(registration_token) + expect(subject.token).not_to eq(group.runners_token) + expect(subject).to be_group_type + end + + context 'when it exceeds the application limits' do + before do + create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago) + create(:plan_limits, :default_plan, ci_registered_group_runners: 1) + end + + it 'does not create runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.persisted?).to be_falsey + expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded']) + expect(group.runners.reload.size).to eq(1) + end + end + + context 'when abandoned runners cause application limits to not be exceeded' do + before do + create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago) + create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago) + create(:plan_limits, :default_plan, ci_registered_group_runners: 1) + end + + it 'creates runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.errors).to be_empty + expect(group.runners.reload.size).to eq(3) + expect(group.runners.recent.size).to eq(1) + end + end + + context 'when valid runner registrars do not include group' do + before do + stub_application_setting(valid_runner_registrars: ['project']) + end + + context 'when feature flag is enabled' do + before do + stub_feature_flags(runner_registration_control: true) + end + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when feature flag is disabled' do + it 'registers the runner' do + is_expected.to be_an_instance_of(::Ci::Runner) + expect(subject.errors).to be_empty + expect(subject.active).to be true + end + end + end + end + end + end +end diff --git a/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb b/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb index 3c461c91ff0..92c46cf7052 100644 --- a/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb +++ b/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb @@ -18,24 +18,29 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do subject { described_class.new(user, params) } - it 'creates a project and returns a successful response' do - stub_headers_for(remote_url, { - 'content-type' => 'application/gzip', - 'content-length' => '10' - }) + shared_examples 'successfully import' do |content_type| + it 'creates a project and returns a successful response' do + stub_headers_for(remote_url, { + 'content-type' => content_type, + 'content-length' => '10' + }) - response = nil - expect { response = subject.execute } - .to change(Project, :count).by(1) + response = nil + expect { response = subject.execute } + .to change(Project, :count).by(1) - expect(response).to be_success - expect(response.http_status).to eq(:ok) - expect(response.payload).to be_instance_of(Project) - expect(response.payload.name).to eq('name') - expect(response.payload.path).to eq('path') - expect(response.payload.namespace).to eq(user.namespace) + expect(response).to be_success + expect(response.http_status).to eq(:ok) + expect(response.payload).to be_instance_of(Project) + expect(response.payload.name).to eq('name') + expect(response.payload.path).to eq('path') + expect(response.payload.namespace).to eq(user.namespace) + end end + it_behaves_like 'successfully import', 'application/gzip' + it_behaves_like 'successfully import', 'application/x-tar' + context 'when the file url is invalid' do it 'returns an erred response with the reason of the failure' do stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) @@ -79,7 +84,7 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do expect(response).not_to be_success expect(response.http_status).to eq(:bad_request) expect(response.message) - .to eq("Remote file content type 'application/js' not allowed. (Allowed content types: application/gzip)") + .to eq("Remote file content type 'application/js' not allowed. (Allowed content types: application/gzip, application/x-tar)") end end @@ -130,6 +135,20 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do end end + it 'does not validate content-type or content-length when the file is stored in AWS-S3' do + stub_headers_for(remote_url, { + 'Server' => 'AmazonS3', + 'x-amz-request-id' => 'Something' + }) + + response = nil + expect { response = subject.execute } + .to change(Project, :count) + + expect(response).to be_success + expect(response.http_status).to eq(:ok) + end + context 'when required parameters are not provided' do let(:params) { {} } diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 3d52ed30c62..19b418d1d6d 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -124,6 +124,12 @@ RSpec.shared_examples 'namespace traversal scopes' do it { expect(subject[0, 2]).to contain_exactly(group_1, group_2) } it { expect(subject[2, 2]).to contain_exactly(nested_group_1, nested_group_2) } end + + context 'with offset and limit' do + subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).offset(1).limit(1).self_and_ancestors } + + it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } + end end describe '.self_and_ancestors' do @@ -168,6 +174,19 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(group_1.id, group_2.id) } end + + context 'with offset and limit' do + subject do + described_class + .where(id: [deep_nested_group_1, deep_nested_group_2]) + .limit(1) + .offset(1) + .self_and_ancestor_ids + .pluck(:id) + end + + it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) } + end end describe '.self_and_ancestor_ids' do diff --git a/spec/support_specs/database/multiple_databases_spec.rb b/spec/support_specs/database/multiple_databases_spec.rb index a8692e315fe..66056359458 100644 --- a/spec/support_specs/database/multiple_databases_spec.rb +++ b/spec/support_specs/database/multiple_databases_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'Database::MultipleDatabases' do context 'when reconnect is true' do it 'does not raise exception' do with_reestablished_active_record_base(reconnect: true) do - expect { ActiveRecord::Base.connection.execute("SELECT 1") }.not_to raise_error # rubocop:disable Database/MultipleDatabases + expect { ApplicationRecord.connection.execute("SELECT 1") }.not_to raise_error end end end @@ -50,7 +50,7 @@ RSpec.describe 'Database::MultipleDatabases' do context 'when reconnect is false' do it 'does raise exception' do with_reestablished_active_record_base(reconnect: false) do - expect { ActiveRecord::Base.connection.execute("SELECT 1") }.to raise_error(ActiveRecord::ConnectionNotEstablished) # rubocop:disable Database/MultipleDatabases + expect { ApplicationRecord.connection.execute("SELECT 1") }.to raise_error(ActiveRecord::ConnectionNotEstablished) end end end