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