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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue45
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql1
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql4
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss23
-rw-r--r--app/assets/stylesheets/pages/alert_management/list.scss13
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/projects/jobs_controller.rb40
-rw-r--r--app/controllers/projects/web_ide_terminals_controller.rb98
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/build_runner_session.rb15
-rw-r--r--app/models/ci/pipeline_enums.rb2
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_import_state.rb6
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/web_ide_terminal.rb51
-rw-r--r--app/policies/ci/build_policy.rb22
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/serializers/web_ide_terminal_entity.rb12
-rw-r--r--app/serializers/web_ide_terminal_serializer.rb11
-rw-r--r--app/services/ci/create_web_ide_terminal_service.rb123
-rw-r--r--app/services/ci/web_ide_config_service.rb59
-rw-r--r--app/services/clusters/parse_cluster_applications_artifact_service.rb27
-rw-r--r--app/services/groups/import_export/export_service.rb23
-rw-r--r--app/services/groups/import_export/import_service.rb5
-rw-r--r--app/services/projects/after_import_service.rb8
-rw-r--r--app/services/projects/import_export/export_service.rb14
-rw-r--r--app/workers/repository_import_worker.rb7
-rw-r--r--app/workers/stuck_import_jobs_worker.rb6
-rwxr-xr-xbin/web20
-rw-r--r--changelogs/unreleased/lm-sorting-list.yml5
-rw-r--r--changelogs/unreleased/tidy_put_projects_issues_spec.yml5
-rw-r--r--changelogs/unreleased/tr-avoid-alert-refetch.yml5
-rw-r--r--config/routes/project.rb15
-rw-r--r--doc/administration/logs.md10
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/development/testing_guide/end_to_end/index.md10
-rw-r--r--doc/install/aws/img/rds_subnet_group.pngbin30107 -> 0 bytes
-rw-r--r--doc/install/aws/index.md14
-rw-r--r--doc/user/project/service_desk.md24
-rw-r--r--lib/gitlab/export/logger.rb11
-rw-r--r--lib/gitlab/github_import/importer/lfs_objects_importer.rb5
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_importer.rb6
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb2
-rw-r--r--lib/gitlab/import_export/attributes_permitter.rb105
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb8
-rw-r--r--lib/gitlab/import_export/saver.rb8
-rw-r--r--lib/gitlab/import_export/version_checker.rb6
-rw-r--r--lib/gitlab/web_ide/config.rb44
-rw-r--r--lib/gitlab/web_ide/config/entry/global.rb29
-rw-r--r--lib/gitlab/web_ide/config/entry/terminal.rb75
-rwxr-xr-xlib/support/init.d/gitlab56
-rw-r--r--locale/gitlab.pot3
-rw-r--r--package.json4
-rwxr-xr-xscripts/gemfile_lock_changed.sh2
-rwxr-xr-xscripts/lint-changelog-filenames4
-rw-r--r--scripts/sync-stable-branch.sh54
-rw-r--r--scripts/utils.sh26
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb194
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/web_ide_terminals_controller_spec.rb304
-rw-r--r--spec/factories/ci/pipelines.rb5
-rw-r--r--spec/features/groups/analytics/cycle_analytics_spec.rb60
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js32
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/attributes_permitter_spec.rb77
-rw-r--r--spec/lib/gitlab/web_ide/config/entry/global_spec.rb164
-rw-r--r--spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb156
-rw-r--r--spec/lib/gitlab/web_ide/config_spec.rb78
-rw-r--r--spec/models/ci/build_runner_session_spec.rb60
-rw-r--r--spec/models/project_import_state_spec.rb11
-rw-r--r--spec/models/web_ide_terminal_spec.rb82
-rw-r--r--spec/policies/ci/build_policy_spec.rb125
-rw-r--r--spec/policies/project_policy_spec.rb58
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb206
-rw-r--r--spec/requests/api/runner_spec.rb59
-rw-r--r--spec/serializers/web_ide_terminal_entity_spec.rb27
-rw-r--r--spec/serializers/web_ide_terminal_serializer_spec.rb27
-rw-r--r--spec/services/ci/create_web_ide_terminal_service_spec.rb143
-rw-r--r--spec/services/ci/web_ide_config_service_spec.rb91
-rw-r--r--spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb14
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb17
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb6
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb6
-rw-r--r--yarn.lock18
84 files changed, 2916 insertions, 310 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index d42e7d760b7..a1a71e592f6 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -21,11 +21,12 @@ import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { capitalizeFirstCharacter, convertToSnakeCase } from '~/lib/utils/text_utility';
const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 hover-bg-blue-50 hover-gl-cursor-pointer hover-gl-border-b-solid hover-gl-border-blue-200';
+const findDefaultSortColumn = () => document.querySelector('.js-started-at');
export default {
i18n: {
@@ -41,34 +42,41 @@ export default {
key: 'severity',
label: s__('AlertManagement|Severity'),
tdClass: `${tdClass} rounded-top text-capitalize`,
+ sortable: true,
},
{
- key: 'startedAt',
+ key: 'startTime',
label: s__('AlertManagement|Start time'),
+ thClass: 'js-started-at',
tdClass,
+ sortable: true,
},
{
- key: 'endedAt',
+ key: 'endTime',
label: s__('AlertManagement|End time'),
tdClass,
+ sortable: true,
},
{
key: 'title',
label: s__('AlertManagement|Alert'),
- thClass: 'w-30p',
+ thClass: 'w-30p alert-title',
tdClass,
+ sortable: false,
},
{
- key: 'eventCount',
+ key: 'eventsCount',
label: s__('AlertManagement|Events'),
- thClass: 'text-right gl-pr-9',
+ thClass: 'text-right gl-pr-9 w-3rem',
tdClass: `${tdClass} text-md-right`,
+ sortable: true,
},
{
key: 'status',
thClass: 'w-15p',
label: s__('AlertManagement|Status'),
tdClass: `${tdClass} rounded-bottom`,
+ sortable: true,
},
],
statuses: {
@@ -122,6 +130,7 @@ export default {
return {
projectPath: this.projectPath,
statuses: this.statusFilter,
+ sort: this.sort,
};
},
update(data) {
@@ -148,6 +157,7 @@ export default {
errored: false,
isAlertDismissed: false,
isErrorAlertDismissed: false,
+ sort: 'START_TIME_ASC',
statusFilter: this.$options.statusTabs[4].filters,
};
},
@@ -170,10 +180,22 @@ export default {
return !this.loading && this.hasAlerts ? bodyTrClass : '';
},
},
+ mounted() {
+ findDefaultSortColumn().ariaSort = 'ascending';
+ },
methods: {
filterAlertsByStatus(tabIndex) {
this.statusFilter = this.$options.statusTabs[tabIndex].filters;
},
+ fetchSortedData({ sortBy, sortDesc }) {
+ const sortDirection = sortDesc ? 'DESC' : 'ASC';
+ const sortColumn = convertToSnakeCase(sortBy).toUpperCase();
+
+ if (sortBy !== 'startTime') {
+ findDefaultSortColumn().ariaSort = 'none';
+ }
+ this.sort = `${sortColumn}_${sortDirection}`;
+ },
capitalizeFirstCharacter,
updateAlertStatus(status, iid) {
this.$apollo
@@ -235,7 +257,10 @@ export default {
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
+ :no-local-sorting="true"
+ sort-icon-left
@row-clicked="navigateToAlertDetails"
+ @sort-changed="fetchSortedData"
>
<template #cell(severity)="{ item }">
<div
@@ -252,13 +277,17 @@ export default {
</div>
</template>
- <template #cell(startedAt)="{ item }">
+ <template #cell(startTime)="{ item }">
<time-ago v-if="item.startedAt" :time="item.startedAt" />
</template>
- <template #cell(endedAt)="{ item }">
+ <template #cell(endTime)="{ item }">
<time-ago v-if="item.endedAt" :time="item.endedAt" />
</template>
+ <!-- TODO: Remove after: https://gitlab.com/gitlab-org/gitlab/-/issues/218467 -->
+ <template #cell(eventsCount)="{ item }">
+ {{ item.eventCount }}
+ </template>
<template #cell(title)="{ item }">
<div class="gl-max-w-full text-truncate">{{ item.title }}</div>
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
index 009ae0b2930..09151f233f5 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
@@ -4,6 +4,7 @@ mutation ($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
alert {
iid,
status,
+ endedAt
}
}
}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
index 294467d6bd1..42e5282e174 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
@@ -1,8 +1,8 @@
#import "../fragments/list_item.fragment.graphql"
-query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!]) {
+query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!], $sort: AlertManagementAlertSort ) {
project(fullPath: $projectPath) {
- alertManagementAlerts(statuses: $statuses) {
+ alertManagementAlerts(statuses: $statuses, sort: $sort) {
nodes {
...AlertListItem
}
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index ef75dabbda4..c6ff7de402e 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -55,3 +55,26 @@ $tooltip-padding-y: 0.5rem;
$tooltip-padding-x: 0.75rem;
$tooltip-arrow-height: 0.5rem;
$tooltip-arrow-width: 1rem;
+$b-table-sort-icon-bg-ascending: url('data:image/svg+xml, <svg \
+ xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"> \
+ <path style="fill: #666;" fill-rule="evenodd" d="M11.707085,11.7071 \
+ L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, \
+ 10.6834 4.292875,10.2929 C4.683375,9.90237 \
+ 5.316575,9.90237 5.707075,10.2929 L6.999975, \
+ 11.5858 L6.999975,2 C6.999975,1.44771 \
+ 7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 \
+ 8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 \
+ ,9.90237 11.316555,9.90237 11.707085,10.2929 \
+ C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/> \
+ </svg>') !default;
+$b-table-sort-icon-bg-descending: url('data:image/svg+xml,<svg \
+ xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"> \
+ <path style="fill: #666;" fill-rule="evenodd" d="M4.29289,4.2971 L8,0.59 \
+ L11.7071,4.2971 C12.0976,4.6876 \
+ 12.0976,5.3208 11.7071,5.7113 C11.3166,6.10183 10.6834, \
+ 6.10183 10.2929,5.7113 L9,4.4184 L9,14.0042 C9,14.55649 \
+ 8.55228,15.0042 8,15.0042 C7.44772,15.0042 7,14.55649 \
+ 7,14.0042 L7,4.4184 L5.70711,5.7113 C5.31658,6.10183 4.68342,6.10183 4.29289,5.7113 \
+ C3.90237,5.3208 3.90237,4.6876 4.29289,4.2971 Z"/> \
+ </svg> ') !default;
+$b-table-sort-icon-bg-not-sorted: '';
diff --git a/app/assets/stylesheets/pages/alert_management/list.scss b/app/assets/stylesheets/pages/alert_management/list.scss
index c5930a087c9..2eee093dfce 100644
--- a/app/assets/stylesheets/pages/alert_management/list.scss
+++ b/app/assets/stylesheets/pages/alert_management/list.scss
@@ -28,8 +28,19 @@
td,
th {
- @include gl-p-5;
+ // TODO: There is no gl-pl-9 utlity for this padding, to be done and then removed.
+ padding-left: 1.25rem;
+ @include gl-py-5;
+ @include gl-outline-none;
border: 0; // Remove cell border styling so that we can set border styling per row
+
+ &.event-count {
+ @include gl-pr-9;
+ }
+
+ &.alert-title {
+ @include gl-pointer-events-none;
+ }
}
th {
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 8a838db04f9..2bf7bdd1ae0 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -6,9 +6,11 @@ class IdeController < ApplicationController
include ClientsidePreviewCSP
include StaticObjectExternalStorageCSP
+ before_action do
+ push_frontend_feature_flag(:build_service_proxy)
+ end
+
def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
end
end
-
-IdeController.prepend_if_ee('EE::IdeController')
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index e0457925b34..e1f6cbe3dca 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -14,6 +14,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action only: [:show] do
push_frontend_feature_flag(:job_log_json, project, default_enabled: true)
end
+ before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
+ before_action :verify_proxy_request!, only: :proxy_websocket_authorize
layout 'project'
@@ -151,6 +153,10 @@ class Projects::JobsController < Projects::ApplicationController
render json: Gitlab::Workhorse.channel_websocket(@build.terminal_specification)
end
+ def proxy_websocket_authorize
+ render json: proxy_websocket_service(build_service_specification)
+ end
+
private
def authorize_update_build!
@@ -165,10 +171,19 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :create_build_terminal, build)
end
+ def authorize_create_proxy_build!
+ return access_denied! unless can?(current_user, :create_build_service_proxy, build)
+ end
+
def verify_api_request!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
+ def verify_proxy_request!
+ verify_api_request!
+ set_workhorse_internal_api_content_type
+ end
+
def raw_send_params
{ type: 'text/plain; charset=utf-8', disposition: 'inline' }
end
@@ -202,6 +217,27 @@ class Projects::JobsController < Projects::ApplicationController
'attachment'
end
-end
-Projects::JobsController.prepend_if_ee('EE::Projects::JobsController')
+ def build_service_specification
+ build.service_specification(service: params['service'],
+ port: params['port'],
+ path: params['path'],
+ subprotocols: proxy_subprotocol)
+ end
+
+ def proxy_subprotocol
+ # This will allow to reuse the same subprotocol set
+ # in the original websocket connection
+ request.headers['HTTP_SEC_WEBSOCKET_PROTOCOL'].presence || ::Ci::BuildRunnerSession::TERMINAL_SUBPROTOCOL
+ end
+
+ # This method provides the information to Workhorse
+ # about the service we want to proxy to.
+ # For security reasons, in case this operation is started by JS,
+ # it's important to use only sourced GitLab JS code
+ def proxy_websocket_service(service)
+ service[:url] = ::Gitlab::UrlHelpers.as_wss(service[:url])
+
+ ::Gitlab::Workhorse.channel_websocket(service)
+ end
+end
diff --git a/app/controllers/projects/web_ide_terminals_controller.rb b/app/controllers/projects/web_ide_terminals_controller.rb
new file mode 100644
index 00000000000..08ea5c4bca8
--- /dev/null
+++ b/app/controllers/projects/web_ide_terminals_controller.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+class Projects::WebIdeTerminalsController < Projects::ApplicationController
+ before_action :authenticate_user!
+
+ before_action :build, except: [:check_config, :create]
+ before_action :authorize_create_web_ide_terminal!
+ before_action :authorize_read_web_ide_terminal!, except: [:check_config, :create]
+ before_action :authorize_update_web_ide_terminal!, only: [:cancel, :retry]
+
+ def check_config
+ return respond_422 unless branch_sha
+
+ result = ::Ci::WebIdeConfigService.new(project, current_user, sha: branch_sha).execute
+
+ if result[:status] == :success
+ head :ok
+ else
+ respond_422
+ end
+ end
+
+ def show
+ render_terminal(build)
+ end
+
+ def create
+ result = ::Ci::CreateWebIdeTerminalService.new(project,
+ current_user,
+ ref: params[:branch])
+ .execute
+
+ if result[:status] == :error
+ render status: :bad_request, json: result[:message]
+ else
+ pipeline = result[:pipeline]
+ current_build = pipeline.builds.last
+
+ if current_build
+ Gitlab::UsageDataCounters::WebIdeCounter.increment_terminals_count
+
+ render_terminal(current_build)
+ else
+ render status: :bad_request, json: pipeline.errors.full_messages
+ end
+ end
+ end
+
+ def cancel
+ return respond_422 unless build.cancelable?
+
+ build.cancel
+
+ head :ok
+ end
+
+ def retry
+ return respond_422 unless build.retryable?
+
+ new_build = Ci::Build.retry(build, current_user)
+
+ render_terminal(new_build)
+ end
+
+ private
+
+ def authorize_create_web_ide_terminal!
+ return access_denied! unless can?(current_user, :create_web_ide_terminal, project)
+ end
+
+ def authorize_read_web_ide_terminal!
+ authorize_build_ability!(:read_web_ide_terminal)
+ end
+
+ def authorize_update_web_ide_terminal!
+ authorize_build_ability!(:update_web_ide_terminal)
+ end
+
+ def authorize_build_ability!(ability)
+ return access_denied! unless can?(current_user, ability, build)
+ end
+
+ def build
+ @build ||= project.builds.find(params[:id])
+ end
+
+ def branch_sha
+ return unless params[:branch].present?
+
+ project.commit(params[:branch])&.id
+ end
+
+ def render_terminal(current_build)
+ render json: WebIdeTerminalSerializer
+ .new(project: project, current_user: current_user)
+ .represent(current_build)
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 2b67bc543b5..c4930d831df 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -55,6 +55,7 @@ module Ci
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
+ delegate :service_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index b46bbe69c7c..bc7f17f046c 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -7,6 +7,8 @@ module Ci
extend Gitlab::Ci::Model
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
+ DEFAULT_SERVICE_NAME = 'build'.freeze
+ DEFAULT_PORT_NAME = 'default_port'.freeze
self.table_name = 'ci_builds_runner_session'
@@ -23,6 +25,17 @@ module Ci
channel_specification(wss_url, TERMINAL_SUBPROTOCOL)
end
+ def service_specification(service: nil, path: nil, port: nil, subprotocols: nil)
+ return {} unless url.present?
+
+ port = port.presence || DEFAULT_PORT_NAME
+ service = service.presence || DEFAULT_SERVICE_NAME
+ url = "#{self.url}/proxy/#{service}/#{port}/#{path}"
+ subprotocols = subprotocols.presence || ::Ci::BuildRunnerSession::TERMINAL_SUBPROTOCOL
+
+ channel_specification(url, subprotocols)
+ end
+
private
def channel_specification(url, subprotocol)
@@ -37,5 +50,3 @@ module Ci
end
end
end
-
-Ci::BuildRunnerSession.prepend_if_ee('EE::Ci::BuildRunnerSession')
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 7e203cb67c4..0b971276a4b 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -27,6 +27,7 @@ module Ci
# https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7,
chat: 8,
+ webide: 9,
merge_request_event: 10,
external_pull_request_event: 11,
parent_pipeline: 12
@@ -40,6 +41,7 @@ module Ci
unknown_source: nil,
repository_source: 1,
auto_devops_source: 2,
+ webide_source: 3,
remote_source: 4,
external_project_source: 5,
bridge_source: 6
diff --git a/app/models/project.rb b/app/models/project.rb
index 314f24f3f6d..795c2c5f1bd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -328,6 +328,8 @@ class Project < ApplicationRecord
has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove'
+ has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -733,6 +735,10 @@ class Project < ApplicationRecord
end
end
+ def active_webide_pipelines(user:)
+ webide_pipelines.running_or_pending.for_user(user)
+ end
+
def autoclose_referenced_issues
return true if super.nil?
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index dfe660ac780..4bd3ffbea2f 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -84,7 +84,11 @@ class ProjectImportState < ApplicationRecord
update_column(:last_error, sanitized_message)
rescue ActiveRecord::ActiveRecordError => e
- Gitlab::AppLogger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
+ Gitlab::Import::Logger.error(
+ message: 'Error setting import status to failed',
+ error: e.message,
+ original_error: sanitized_message
+ )
ensure
@errors = original_errors
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 021b1e60646..3af53d06922 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -69,7 +69,6 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180
- ignore_column :bot_type, remove_with: '13.1', remove_after: '2020-05-22'
ignore_column :ghost, remove_with: '13.2', remove_after: '2020-06-22'
# Override Devise::Models::Trackable#update_tracked_fields!
diff --git a/app/models/web_ide_terminal.rb b/app/models/web_ide_terminal.rb
new file mode 100644
index 00000000000..ef70df2405f
--- /dev/null
+++ b/app/models/web_ide_terminal.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class WebIdeTerminal
+ include ::Gitlab::Routing
+
+ attr_reader :build, :project
+
+ delegate :id, :status, to: :build
+
+ def initialize(build)
+ @build = build
+ @project = build.project
+ end
+
+ def show_path
+ web_ide_terminal_route_generator(:show)
+ end
+
+ def retry_path
+ web_ide_terminal_route_generator(:retry)
+ end
+
+ def cancel_path
+ web_ide_terminal_route_generator(:cancel)
+ end
+
+ def terminal_path
+ terminal_project_job_path(project, build, format: :ws)
+ end
+
+ def proxy_websocket_path
+ proxy_project_job_path(project, build, format: :ws)
+ end
+
+ def services
+ build.services.map(&:alias).compact + Array(build.image&.alias)
+ end
+
+ private
+
+ def web_ide_terminal_route_generator(action, options = {})
+ options.reverse_merge!(action: action,
+ controller: 'projects/web_ide_terminals',
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: build.id,
+ only_path: true)
+
+ url_for(options)
+ end
+end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 12892a69257..0879a740f8a 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -36,6 +36,10 @@ module Ci
@subject.has_terminal?
end
+ condition(:is_web_ide_terminal, scope: :subject) do
+ @subject.pipeline.webide?
+ end
+
rule { protected_ref | archived }.policy do
prevent :update_build
prevent :update_commit_status
@@ -50,6 +54,24 @@ module Ci
end
rule { can?(:update_build) & terminal }.enable :create_build_terminal
+
+ rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
+ enable :read_web_ide_terminal
+ enable :update_web_ide_terminal
+ end
+
+ rule { is_web_ide_terminal & ~can?(:update_web_ide_terminal) }.policy do
+ prevent :create_build_terminal
+ end
+
+ rule { can?(:update_web_ide_terminal) & terminal }.policy do
+ enable :create_build_terminal
+ enable :create_build_service_proxy
+ end
+
+ rule { ~can?(:build_service_proxy_enabled) }.policy do
+ prevent :create_build_service_proxy
+ end
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 44de17121de..93f18a350ad 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -147,6 +147,10 @@ class ProjectPolicy < BasePolicy
@user && @user.confirmed?
end
+ condition(:build_service_proxy_enabled) do
+ ::Feature.enabled?(:build_service_proxy, @subject)
+ end
+
features = %w[
merge_requests
issues
@@ -559,6 +563,10 @@ class ProjectPolicy < BasePolicy
enable :read_project
end
+ rule { can?(:create_pipeline) & can?(:maintainer_access) }.enable :create_web_ide_terminal
+
+ rule { build_service_proxy_enabled }.enable :build_service_proxy_enabled
+
private
def team_member?
diff --git a/app/serializers/web_ide_terminal_entity.rb b/app/serializers/web_ide_terminal_entity.rb
new file mode 100644
index 00000000000..e2e90e824e7
--- /dev/null
+++ b/app/serializers/web_ide_terminal_entity.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class WebIdeTerminalEntity < Grape::Entity
+ expose :id
+ expose :status
+ expose :show_path
+ expose :cancel_path
+ expose :retry_path
+ expose :terminal_path
+ expose :services
+ expose :proxy_websocket_path, if: ->(_) { Feature.enabled?(:build_service_proxy) }
+end
diff --git a/app/serializers/web_ide_terminal_serializer.rb b/app/serializers/web_ide_terminal_serializer.rb
new file mode 100644
index 00000000000..5a9c4b99e0a
--- /dev/null
+++ b/app/serializers/web_ide_terminal_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class WebIdeTerminalSerializer < BaseSerializer
+ entity WebIdeTerminalEntity
+
+ def represent(resource, opts = {})
+ resource = WebIdeTerminal.new(resource) if resource.is_a?(Ci::Build)
+
+ super
+ end
+end
diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb
new file mode 100644
index 00000000000..29d40756ab4
--- /dev/null
+++ b/app/services/ci/create_web_ide_terminal_service.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+module Ci
+ class CreateWebIdeTerminalService < ::BaseService
+ include ::Gitlab::Utils::StrongMemoize
+
+ TerminalCreationError = Class.new(StandardError)
+
+ TERMINAL_NAME = 'terminal'.freeze
+
+ attr_reader :terminal
+
+ def execute
+ check_access!
+ validate_params!
+ load_terminal_config!
+
+ pipeline = create_pipeline!
+ success(pipeline: pipeline)
+ rescue TerminalCreationError => e
+ error(e.message)
+ rescue ActiveRecord::RecordInvalid => e
+ error("Failed to persist the pipeline: #{e.message}")
+ end
+
+ private
+
+ def create_pipeline!
+ build_pipeline.tap do |pipeline|
+ pipeline.stages << terminal_stage_seed(pipeline).to_resource
+ pipeline.save!
+
+ Ci::ProcessPipelineService
+ .new(pipeline)
+ .execute(nil, initial_process: true)
+
+ pipeline_created_counter.increment(source: :webide)
+ end
+ end
+
+ def build_pipeline
+ Ci::Pipeline.new(
+ project: project,
+ user: current_user,
+ source: :webide,
+ config_source: :webide_source,
+ ref: ref,
+ sha: sha,
+ tag: false,
+ before_sha: Gitlab::Git::BLANK_SHA
+ )
+ end
+
+ def terminal_stage_seed(pipeline)
+ attributes = {
+ name: TERMINAL_NAME,
+ index: 0,
+ builds: [terminal_build_seed]
+ }
+
+ Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, [])
+ end
+
+ def terminal_build_seed
+ terminal.merge(
+ name: TERMINAL_NAME,
+ stage: TERMINAL_NAME,
+ user: current_user,
+ scheduling_type: :stage)
+ end
+
+ def load_terminal_config!
+ result = ::Ci::WebIdeConfigService.new(project, current_user, sha: sha).execute
+ raise TerminalCreationError, result[:message] if result[:status] != :success
+
+ @terminal = result[:terminal]
+ raise TerminalCreationError, 'Terminal is not configured' unless terminal
+ end
+
+ def validate_params!
+ unless sha
+ raise TerminalCreationError, 'Ref does not exist'
+ end
+
+ unless branch_exists?
+ raise TerminalCreationError, 'Ref needs to be a branch'
+ end
+ end
+
+ def check_access!
+ unless can?(current_user, :create_web_ide_terminal, project)
+ raise TerminalCreationError, 'Insufficient permissions to create a terminal'
+ end
+
+ if terminal_active?
+ raise TerminalCreationError, 'There is already a terminal running'
+ end
+ end
+
+ def pipeline_created_counter
+ @pipeline_created_counter ||= Gitlab::Metrics
+ .counter(:pipelines_created_total, "Counter of pipelines created")
+ end
+
+ def terminal_active?
+ project.active_webide_pipelines(user: current_user).exists?
+ end
+
+ def ref
+ strong_memoize(:ref) do
+ Gitlab::Git.ref_name(params[:ref])
+ end
+ end
+
+ def branch_exists?
+ project.repository.branch_exists?(ref)
+ end
+
+ def sha
+ project.commit(params[:ref]).try(:id)
+ end
+ end
+end
diff --git a/app/services/ci/web_ide_config_service.rb b/app/services/ci/web_ide_config_service.rb
new file mode 100644
index 00000000000..ade9132f419
--- /dev/null
+++ b/app/services/ci/web_ide_config_service.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Ci
+ class WebIdeConfigService < ::BaseService
+ include ::Gitlab::Utils::StrongMemoize
+
+ ValidationError = Class.new(StandardError)
+
+ WEBIDE_CONFIG_FILE = '.gitlab/.gitlab-webide.yml'.freeze
+
+ attr_reader :config, :config_content
+
+ def execute
+ check_access!
+ load_config_content!
+ load_config!
+
+ success(terminal: config.terminal_value)
+ rescue ValidationError => e
+ error(e.message)
+ end
+
+ private
+
+ def check_access!
+ unless can?(current_user, :download_code, project)
+ raise ValidationError, 'Insufficient permissions to read configuration'
+ end
+ end
+
+ def load_config_content!
+ @config_content = webide_yaml_from_repo
+
+ unless config_content
+ raise ValidationError, "Failed to load Web IDE config file '#{WEBIDE_CONFIG_FILE}' for #{params[:sha]}"
+ end
+ end
+
+ def load_config!
+ @config = Gitlab::WebIde::Config.new(config_content)
+
+ unless @config.valid?
+ raise ValidationError, @config.errors.first
+ end
+ rescue Gitlab::WebIde::Config::ConfigError => e
+ raise ValidationError, e.message
+ end
+
+ def webide_yaml_from_repo
+ gitlab_webide_yml_for(params[:sha])
+ rescue GRPC::NotFound, GRPC::Internal
+ nil
+ end
+
+ def gitlab_webide_yml_for(sha)
+ project.repository.blob_data_at(sha, WEBIDE_CONFIG_FILE)
+ end
+ end
+end
diff --git a/app/services/clusters/parse_cluster_applications_artifact_service.rb b/app/services/clusters/parse_cluster_applications_artifact_service.rb
index b8e1c80cfe7..35fba5f47c7 100644
--- a/app/services/clusters/parse_cluster_applications_artifact_service.rb
+++ b/app/services/clusters/parse_cluster_applications_artifact_service.rb
@@ -18,13 +18,9 @@ module Clusters
raise ArgumentError, 'Artifact is not cluster_applications file type' unless artifact&.cluster_applications?
- unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE
- return error(too_big_error_message, :bad_request)
- end
-
- unless cluster
- return error(s_('ClusterIntegration|No deployment cluster found for this job'))
- end
+ return error(too_big_error_message, :bad_request) unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE
+ return error(no_deployment_message, :bad_request) unless job.deployment
+ return error(no_deployment_cluster_message, :bad_request) unless cluster
parse!(artifact)
@@ -61,7 +57,8 @@ module Clusters
Clusters::Cluster.transaction do
RELEASE_NAMES.each do |release_name|
- application = find_or_build_application(release_name)
+ application_class = Clusters::Cluster::APPLICATIONS[release_name]
+ application = cluster.find_or_build_application(application_class)
release = release_by_name[release_name]
@@ -80,16 +77,18 @@ module Clusters
end
end
- def find_or_build_application(application_name)
- application_class = Clusters::Cluster::APPLICATIONS[application_name]
-
- cluster.find_or_build_application(application_class)
- end
-
def too_big_error_message
human_size = ActiveSupport::NumberHelper.number_to_human_size(MAX_ACCEPTABLE_ARTIFACT_SIZE)
s_('ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}') % { human_size: human_size }
end
+
+ def no_deployment_message
+ s_('ClusterIntegration|No deployment found for this job')
+ end
+
+ def no_deployment_cluster_message
+ s_('ClusterIntegration|No deployment cluster found for this job')
+ end
end
end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index 0f2e3bb65f9..39a6889fc84 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -4,10 +4,11 @@ module Groups
module ImportExport
class ExportService
def initialize(group:, user:, params: {})
- @group = group
+ @group = group
@current_user = user
- @params = params
- @shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group)
+ @params = params
+ @shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group)
+ @logger = Gitlab::Export::Logger.build
end
def async_execute
@@ -91,21 +92,21 @@ module Groups
end
def notify_success
- @shared.logger.info(
- group_id: @group.id,
- group_name: @group.name,
- message: 'Group Import/Export: Export succeeded'
+ @logger.info(
+ message: 'Group Export succeeded',
+ group_id: @group.id,
+ group_name: @group.name
)
notification_service.group_was_exported(@group, @current_user)
end
def notify_error
- @shared.logger.error(
- group_id: @group.id,
+ @logger.error(
+ message: 'Group Export failed',
+ group_id: @group.id,
group_name: @group.name,
- error: @shared.errors.join(', '),
- message: 'Group Import/Export: Export failed'
+ errors: @shared.errors.join(', ')
)
notification_service.group_was_not_exported(@group, @current_user, @shared.errors)
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index e1be40f0241..dcd78210801 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -9,6 +9,7 @@ module Groups
@group = group
@current_user = user
@shared = Gitlab::ImportExport::Shared.new(@group)
+ @logger = Gitlab::Import::Logger.build
end
def async_execute
@@ -81,7 +82,7 @@ module Groups
end
def notify_success
- @shared.logger.info(
+ @logger.info(
group_id: @group.id,
group_name: @group.name,
message: 'Group Import/Export: Import succeeded'
@@ -89,7 +90,7 @@ module Groups
end
def notify_error
- @shared.logger.error(
+ @logger.error(
group_id: @group.id,
group_name: @group.name,
message: "Group Import/Export: Errors occurred, see '#{Gitlab::ErrorTracking::Logger.file_name}' for details"
diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb
index b09a8e0bece..fad2290a47b 100644
--- a/app/services/projects/after_import_service.rb
+++ b/app/services/projects/after_import_service.rb
@@ -22,8 +22,12 @@ module Projects
# causing GC to run every time.
service.increment!
rescue Projects::HousekeepingService::LeaseTaken => e
- Gitlab::AppLogger.info(
- "Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}")
+ Gitlab::Import::Logger.info(
+ message: 'Project housekeeping failed',
+ project_full_path: @project.full_path,
+ project_id: @project.id,
+ error: e.message
+ )
end
private
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 0c515479cfa..031b99753c3 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -9,6 +9,7 @@ module Projects
super
@shared = project.import_export_shared
+ @logger = Gitlab::Export::Logger.build
end
def execute(after_export_strategy = nil)
@@ -115,11 +116,20 @@ module Projects
end
def notify_success
- Gitlab::AppLogger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
+ @logger.info(
+ message: 'Project successfully exported',
+ project_name: project.name,
+ project_id: project.id
+ )
end
def notify_error
- Gitlab::AppLogger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}")
+ @logger.error(
+ message: 'Project export error',
+ export_errors: shared.errors.join(', '),
+ project_name: project.name,
+ project_id: project.id
+ )
notification_service.project_not_exported(project, current_user, shared.errors)
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 9f17ef467e3..30570a2227e 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -43,7 +43,12 @@ class RepositoryImportWorker # rubocop:disable Scalability/IdempotentWorker
def start_import
return true if start(project.import_state)
- Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::Import::Logger.info(
+ message: 'Project was in inconsistent state while importing',
+ project_full_path: project.full_path,
+ project_import_status: project.import_status
+ )
+
false
end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index 6a48b78b22c..8e8bf537bd5 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -45,7 +45,11 @@ class StuckImportJobsWorker # rubocop:disable Scalability/IdempotentWorker
completed_import_states = enqueued_import_states_with_jid.where(id: completed_import_state_ids)
completed_import_state_jids = completed_import_states.map { |import_state| import_state.jid }.join(', ')
- Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_import_state_jids}") # rubocop:disable Gitlab/RailsLogger
+
+ Gitlab::Import::Logger.info(
+ message: 'Marked stuck import jobs as failed',
+ job_ids: completed_import_state_jids
+ )
completed_import_states.each do |import_state|
import_state.mark_as_failed(error_message)
diff --git a/bin/web b/bin/web
index 0f5689770a7..b714ad1e1bb 100755
--- a/bin/web
+++ b/bin/web
@@ -5,16 +5,16 @@ set -e
cd $(dirname $0)/..
case "$USE_WEB_SERVER" in
- puma|"") # and the "" defines default
- exec bin/web_puma "$@"
- ;;
+ puma|"") # and the "" defines default
+ exec bin/web_puma "$@"
+ ;;
- unicorn)
- exec bin/web_unicorn "$@"
- ;;
+ unicorn)
+ exec bin/web_unicorn "$@"
+ ;;
- *)
- echo "Unkown web server used by USE_WEB_SERVER: $USE_WEB_SERVER."
- exit 1
- ;;
+ *)
+ echo "Unkown web server used by USE_WEB_SERVER: $USE_WEB_SERVER."
+ exit 1
+ ;;
esac
diff --git a/changelogs/unreleased/lm-sorting-list.yml b/changelogs/unreleased/lm-sorting-list.yml
new file mode 100644
index 00000000000..e1a9dceec5c
--- /dev/null
+++ b/changelogs/unreleased/lm-sorting-list.yml
@@ -0,0 +1,5 @@
+---
+title: Adds sorting by column to alert management list
+merge_request: 32478
+author:
+type: added
diff --git a/changelogs/unreleased/tidy_put_projects_issues_spec.yml b/changelogs/unreleased/tidy_put_projects_issues_spec.yml
new file mode 100644
index 00000000000..ef0d8d1745d
--- /dev/null
+++ b/changelogs/unreleased/tidy_put_projects_issues_spec.yml
@@ -0,0 +1,5 @@
+---
+title: Tidy
+merge_request: 32759
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/tr-avoid-alert-refetch.yml b/changelogs/unreleased/tr-avoid-alert-refetch.yml
new file mode 100644
index 00000000000..3dafc79a366
--- /dev/null
+++ b/changelogs/unreleased/tr-avoid-alert-refetch.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid refresh to show endedAt after mutation
+merge_request: 32636
+author:
+type: fixed
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 020bfa7687d..219cf3a5ea6 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -49,9 +49,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :trace, defaults: { format: 'json' }
get :raw
get :terminal
+ get :proxy
- # This route is also defined in gitlab-workhorse. Make sure to update accordingly.
+ # These routes are also defined in gitlab-workhorse. Make sure to update accordingly.
get '/terminal.ws/authorize', to: 'jobs#terminal_websocket_authorize', format: false
+ get '/proxy.ws/authorize', to: 'jobs#proxy_websocket_authorize', format: false
end
resource :artifacts, only: [] do
@@ -472,6 +474,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :web_ide_pipelines_count
end
+ resources :web_ide_terminals, path: :ide_terminals, only: [:create, :show], constraints: { id: /\d+/, format: :json } do # rubocop: disable Cop/PutProjectRoutesUnderScope
+ member do
+ post :cancel
+ post :retry
+ end
+
+ collection do
+ post :check_config
+ end
+ end
+
# Deprecated unscoped routing.
# Issue https://gitlab.com/gitlab-org/gitlab/issues/118849
scope as: 'deprecated' do
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index c679b69ef8f..9e892a55f92 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -597,6 +597,16 @@ installations from source.
It logs the progress of the import process.
+## `exporter.log`
+
+> Introduced in GitLab 13.1.
+
+This file lives in `/var/log/gitlab/gitlab-rails/exporter.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/exporter.log` for
+installations from source.
+
+It logs the progress of the export process.
+
## `auth.log`
> Introduced in GitLab 12.0.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 2f5fd59a397..2b65c58be24 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1943,6 +1943,7 @@ GET /projects/:id/hooks/:hook_id
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
+ "confidential_note_events": true,
"job_events": true,
"pipeline_events": true,
"wiki_page_events": true,
@@ -1970,6 +1971,7 @@ POST /projects/:id/hooks
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
+| `confidential_note_events` | boolean | no | Trigger hook on confidential note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_page_events` | boolean | no | Trigger hook on wiki events |
@@ -1996,6 +1998,7 @@ PUT /projects/:id/hooks/:hook_id
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
+| `confidential_note_events` | boolean | no | Trigger hook on confidential note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md
index 54c35a07058..32086596a4a 100644
--- a/doc/development/testing_guide/end_to_end/index.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -65,18 +65,18 @@ subgraph "gitlab-qa-mirror pipeline"
end
```
-1. Developer triggers a manual action, that can be found in CE / EE merge
+1. Developer triggers a manual action, that can be found in GitLab merge
requests. This starts a chain of pipelines in multiple projects.
1. The script being executed triggers a pipeline in
- [Omnibus GitLab Mirror](https://gitlab.com/gitlab-org/omnibus-gitlab-mirror)
+ [Omnibus GitLab Mirror](https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror)
and waits for the resulting status. We call this a _status attribution_.
-1. GitLab packages are being built in the [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab)
+1. GitLab packages are being built in the [Omnibus GitLab Mirror](https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror)
pipeline. Packages are then pushed to its Container Registry.
1. When packages are ready, and available in the registry, a final step in the
- [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab) pipeline, triggers a new
+ [Omnibus GitLab Mirror](https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror) pipeline, triggers a new
GitLab QA pipeline (those with access can view them at `https://gitlab.com/gitlab-org/gitlab-qa-mirror/pipelines`). It also waits for a resulting status.
1. GitLab QA pulls images from the registry, spins-up containers and runs tests
@@ -84,7 +84,7 @@ subgraph "gitlab-qa-mirror pipeline"
tool.
1. The result of the GitLab QA pipeline is being
- propagated upstream, through Omnibus, back to the CE / EE merge request.
+ propagated upstream, through Omnibus, back to the GitLab merge request.
Please note, we plan to [add more specific information](https://gitlab.com/gitlab-org/quality/team-tasks/issues/156)
about the tests included in each job/scenario that runs in `gitlab-qa-mirror`.
diff --git a/doc/install/aws/img/rds_subnet_group.png b/doc/install/aws/img/rds_subnet_group.png
deleted file mode 100644
index 7c6157e38e0..00000000000
--- a/doc/install/aws/img/rds_subnet_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 30b10370f1e..ea586749d2d 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -280,7 +280,10 @@ We need a security group for our database that will allow inbound traffic from t
1. From the EC2 dashboard, select **Security Groups** from the left menu bar.
1. Click **Create security group**.
1. Give it a name (we'll use `gitlab-rds-sec-group`), a description, and select the `gitlab-vpc` from the **VPC** dropdown.
-1. In the **Inbound rules** section, click **Add rule** and add a **PostgreSQL** rule, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier. The default PostgreSQL port is `5432`, which we'll also use when creating our database below.
+1. In the **Inbound rules** section, click **Add rule** and set the following:
+ 1. **Type:** search for and select the **PostgreSQL** rule.
+ 1. **Source type:** set as "Custom".
+ 1. **Source:** select the `gitlab-loadbalancer-sec-group` we created earlier.
1. When done, click **Create security group**.
### RDS Subnet Group
@@ -288,11 +291,10 @@ We need a security group for our database that will allow inbound traffic from t
1. Navigate to the RDS dashboard and select **Subnet Groups** from the left menu.
1. Click on **Create DB Subnet Group**.
1. Under **Subnet group details**, enter a name (we'll use `gitlab-rds-group`), a description, and choose the `gitlab-vpc` from the VPC dropdown.
-1. Under **Add subnets**, click **Add all the subnets related to this VPC** and remove the public ones, we only want the **private subnets**. In the end, you should see `10.0.1.0/24` and `10.0.3.0/24` (as we defined them in the [subnets section](#subnets)).
+1. From the **Availability Zones** dropdown, select the Availability Zones that include the subnets you've configured. In our case, we'll add `eu-west-2a` and `eu-west-2b`.
+1. From the **Subnets** dropdown, select the two private subnets (`10.0.1.0/24` and `10.0.3.0/24`) as we defined them in the [subnets section](#subnets).
1. Click **Create** when ready.
- ![RDS Subnet Group](img/rds_subnet_group.png)
-
### Create the database
DANGER: **Danger:** Avoid using burstable instances (t class instances) for the database as this could lead to performance issues due to CPU credits running out during sustained periods of high load.
@@ -301,7 +303,7 @@ Now, it's time to create the database:
1. Navigate to the RDS dashboard, select **Databases** from the left menu, and click **Create database**.
1. Select **Standard Create** for the database creation method.
-1. Select **PostgreSQL** as the database engine and select **PostgreSQL 10.9-R1** from the version dropdown menu (check the [database requirements](../../install/requirements.md#postgresql-requirements) to see if there are any updates on this for your chosen version of GitLab).
+1. Select **PostgreSQL** as the database engine and select the minimum PostgreSQL version as defined for your GitLab version in our [database requirements](../../install/requirements.md#postgresql-requirements).
1. Since this is a production server, let's choose **Production** from the **Templates** section.
1. Under **Settings**, set a DB instance identifier, a master username, and a master password. We'll use `gitlab-db-ha`, `gitlab`, and a very secure password respectively. Make a note of these as we'll need them later.
1. For the DB instance size, select **Standard classes** and select an instance size that meets your requirements from the dropdown menu. We'll use a `db.m4.large` instance.
@@ -329,7 +331,7 @@ Now that the database is created, let's move on to setting up Redis with ElastiC
## Redis with ElastiCache
ElastiCache is an in-memory hosted caching solution. Redis maintains its own
-persistence and is used for certain types of the GitLab application.
+persistence and is used to store session data, temporary cache information, and background job queues for the GitLab application.
### Create a Redis Security Group
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index 582549708ad..b97be36fd0f 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -1,6 +1,7 @@
# Service Desk **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/149) in [GitLab Premium 9.1](https://about.gitlab.com/releases/2017/04/22/gitlab-9-1-released/#service-desk-eep).
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/149) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.1.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/214839) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.0.
## Overview
@@ -28,14 +29,19 @@ with GitLab CI/CD.
Here's how Service Desk will work for you:
-1. You'll provide a project-specific email address to your paying customers, who can email you directly from within the app
-1. Each email they send creates an issue in the appropriate project
-1. Your team members navigate to the Service Desk issue tracker, where they can see new support requests and respond inside associated issues
-1. Your team communicates back and forth with the customer to understand the request
-1. Your team starts working on implementing code to solve your customer's problem
-1. When your team finishes the implementation, whereupon the merge request is merged and the issue is closed automatically
-1. The customer will have been attended successfully via email, without having real access to your GitLab instance
-1. Your team saved time by not having to leave GitLab (or setup any integrations) to follow up with your customer
+1. You provide a project-specific email address to your paying customers, who can email you directly
+ from within the app.
+1. Each email they send creates an issue in the appropriate project.
+1. Your team members navigate to the Service Desk issue tracker, where they can see new support
+ requests and respond inside associated issues.
+1. Your team communicates back and forth with the customer to understand the request.
+1. Your team starts working on implementing code to solve your customer's problem.
+1. When your team finishes the implementation, whereupon the merge request is merged and the issue
+ is closed automatically.
+1. The customer will have been attended successfully via email, without having real access to your
+ GitLab instance.
+1. Your team saved time by not having to leave GitLab (or setup any integrations) to follow up with
+ your customer.
## How it works
diff --git a/lib/gitlab/export/logger.rb b/lib/gitlab/export/logger.rb
new file mode 100644
index 00000000000..b3c05651cd4
--- /dev/null
+++ b/lib/gitlab/export/logger.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Export
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'exporter'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/lfs_objects_importer.rb b/lib/gitlab/github_import/importer/lfs_objects_importer.rb
index 30763492235..5980b3c2179 100644
--- a/lib/gitlab/github_import/importer/lfs_objects_importer.rb
+++ b/lib/gitlab/github_import/importer/lfs_objects_importer.rb
@@ -29,7 +29,10 @@ module Gitlab
yield object
end
rescue StandardError => e
- Rails.logger.error("The Lfs import process failed. #{e.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::Import::Logger.error(
+ message: 'The Lfs import process failed',
+ error: e.message
+ )
end
end
end
diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb
index 929fceaacf2..dcae8ca01fa 100644
--- a/lib/gitlab/github_import/importer/pull_requests_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb
@@ -40,8 +40,10 @@ module Gitlab
pname = project.path_with_namespace
- Rails.logger # rubocop:disable Gitlab/RailsLogger
- .info("GitHub importer finished updating repository for #{pname}")
+ Gitlab::Import::Logger.info(
+ message: 'GitHub importer finished updating repository',
+ project_name: pname
+ )
repository_updates_counter.increment
end
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index bab473741b1..1e98595bb07 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -3,6 +3,8 @@
module Gitlab
module ImportExport
class AttributesFinder
+ attr_reader :tree, :included_attributes, :excluded_attributes, :methods, :preloads
+
def initialize(config:)
@tree = config[:tree] || {}
@included_attributes = config[:included_attributes] || {}
diff --git a/lib/gitlab/import_export/attributes_permitter.rb b/lib/gitlab/import_export/attributes_permitter.rb
new file mode 100644
index 00000000000..86f51add504
--- /dev/null
+++ b/lib/gitlab/import_export/attributes_permitter.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+# AttributesPermitter builds a hash of permitted attributes for
+# every model defined in import_export.yml that is used to validate and
+# filter out any attributes that are not permitted when doing Project/Group Import
+#
+# Each model's list includes:
+# - attributes defined under included_attributes section
+# - associations defined under project/group tree
+# - methods defined under methods section
+#
+# Given the following import_export.yml example:
+# ```
+# tree:
+# project:
+# - labels:
+# - :priorities
+# included_attributes:
+# labels:
+# - :title
+# - :description
+# methods:
+# labels:
+# - :type
+# ```
+#
+# Produces a list of permitted attributes:
+# ```
+# Gitlab::ImportExport::AttributesPermitter.new.permitted_attributes
+#
+# => { labels: [:priorities, :title, :description, :type] }
+# ```
+#
+# Filters out any other attributes from specific relation hash:
+# ```
+# Gitlab::ImportExport::AttributesPermitter.new.permit(:labels, {id: 5, type: 'opened', description: 'test', sensitive_attribute: 'my_sensitive_attribute'})
+#
+# => {:type=>"opened", :description=>"test"}
+# ```
+module Gitlab
+ module ImportExport
+ class AttributesPermitter
+ attr_reader :permitted_attributes
+
+ def initialize(config: ImportExport::Config.new.to_h)
+ @config = config
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(config: @config)
+ @permitted_attributes = {}
+
+ build_permitted_attributes
+ end
+
+ def permit(relation_name, relation_hash)
+ permitted_attributes = permitted_attributes_for(relation_name)
+
+ relation_hash.select do |key, _|
+ permitted_attributes.include?(key)
+ end
+ end
+
+ def permitted_attributes_for(relation_name)
+ @permitted_attributes[relation_name] || []
+ end
+
+ private
+
+ def build_permitted_attributes
+ build_associations
+ build_attributes
+ build_methods
+ end
+
+ # Deep traverse relations tree to build a list of allowed model relations
+ def build_associations
+ stack = @attributes_finder.tree.to_a
+
+ while stack.any?
+ model_name, relations = stack.pop
+
+ if relations.is_a?(Hash)
+ add_permitted_attributes(model_name, relations.keys)
+
+ stack.concat(relations.to_a)
+ end
+ end
+
+ @permitted_attributes
+ end
+
+ def build_attributes
+ @attributes_finder.included_attributes.each(&method(:add_permitted_attributes))
+ end
+
+ def build_methods
+ @attributes_finder.methods.each(&method(:add_permitted_attributes))
+ end
+
+ def add_permitted_attributes(model_name, attributes)
+ @permitted_attributes[model_name] ||= []
+
+ @permitted_attributes[model_name].concat(attributes) if attributes.any?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index f735b9612aa..4643742b607 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -41,7 +41,13 @@ module Gitlab
def create_source_branch
@project.repository.create_branch(@merge_request.source_branch, @diff_head_sha)
rescue => err
- Rails.logger.warn("Import/Export warning: Failed to create source branch #{@merge_request.source_branch} => #{@diff_head_sha} for MR #{@merge_request.iid}: #{err}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::Import::Logger.warn(
+ message: 'Import warning: Failed to create source branch',
+ source_branch: @merge_request.source_branch,
+ diff_head_sha: @diff_head_sha,
+ merge_request_iid: @merge_request.iid,
+ error: err.message
+ )
end
def create_target_branch
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index ae82c380755..bd69673ecdf 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -11,14 +11,18 @@ module Gitlab
def initialize(exportable:, shared:)
@exportable = exportable
- @shared = shared
+ @shared = shared
end
def save
if compress_and_save
remove_export_path
- Rails.logger.info("Saved #{@exportable.class} export #{archive_file}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::Export::Logger.info(
+ message: 'Export archive saved',
+ exportable_class: @exportable.class.to_s,
+ archive_file: archive_file
+ )
save_upload
else
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index 86ea7a30e69..4154d4fe775 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -36,7 +36,11 @@ module Gitlab
def different_version?(version)
Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
rescue => e
- Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::Import::Logger.error(
+ message: 'Import error',
+ error: e.message
+ )
+
raise Gitlab::ImportExport::Error.new('Incorrect VERSION format')
end
end
diff --git a/lib/gitlab/web_ide/config.rb b/lib/gitlab/web_ide/config.rb
new file mode 100644
index 00000000000..3b1fa162b53
--- /dev/null
+++ b/lib/gitlab/web_ide/config.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebIde
+ #
+ # Base GitLab WebIde Configuration facade
+ #
+ class Config
+ ConfigError = Class.new(StandardError)
+
+ def initialize(config, opts = {})
+ @config = build_config(config, opts)
+
+ @global = Entry::Global.new(@config,
+ with_image_ports: true)
+ @global.compose!
+ rescue Gitlab::Config::Loader::FormatError => e
+ raise Config::ConfigError, e.message
+ end
+
+ def valid?
+ @global.valid?
+ end
+
+ def errors
+ @global.errors
+ end
+
+ def to_hash
+ @config
+ end
+
+ def terminal_value
+ @global.terminal_value
+ end
+
+ private
+
+ def build_config(config, opts = {})
+ Gitlab::Config::Loader::Yaml.new(config).load!
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/web_ide/config/entry/global.rb b/lib/gitlab/web_ide/config/entry/global.rb
new file mode 100644
index 00000000000..50c3f2d294f
--- /dev/null
+++ b/lib/gitlab/web_ide/config/entry/global.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebIde
+ class Config
+ module Entry
+ ##
+ # This class represents a global entry - root Entry for entire
+ # GitLab WebIde Configuration file.
+ #
+ class Global < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
+
+ ALLOWED_KEYS = %i[terminal].freeze
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ end
+
+ entry :terminal, Entry::Terminal,
+ description: 'Configuration of the webide terminal.'
+
+ attributes :terminal
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/web_ide/config/entry/terminal.rb b/lib/gitlab/web_ide/config/entry/terminal.rb
new file mode 100644
index 00000000000..403e308d45b
--- /dev/null
+++ b/lib/gitlab/web_ide/config/entry/terminal.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebIde
+ class Config
+ module Entry
+ ##
+ # Entry that represents a concrete CI/CD job.
+ #
+ class Terminal < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
+
+ # By default the build will finish in a few seconds, not giving the webide
+ # enough time to connect to the terminal. This default script provides
+ # those seconds blocking the build from finishing inmediately.
+ DEFAULT_SCRIPT = ['sleep 60'].freeze
+
+ ALLOWED_KEYS = %i[image services tags before_script script variables].freeze
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, job_port_unique: { data: ->(record) { record.ports } }
+
+ with_options allow_nil: true do
+ validates :tags, array_of_strings: true
+ end
+ end
+
+ entry :before_script, ::Gitlab::Ci::Config::Entry::Script,
+ description: 'Global before script overridden in this job.'
+
+ entry :script, ::Gitlab::Ci::Config::Entry::Commands,
+ description: 'Commands that will be executed in this job.'
+
+ entry :image, ::Gitlab::Ci::Config::Entry::Image,
+ description: 'Image that will be used to execute this job.'
+
+ entry :services, ::Gitlab::Ci::Config::Entry::Services,
+ description: 'Services that will be used to execute this job.'
+
+ entry :variables, ::Gitlab::Ci::Config::Entry::Variables,
+ description: 'Environment variables available for this job.'
+
+ attributes :tags
+
+ def value
+ to_hash.compact
+ end
+
+ private
+
+ def to_hash
+ { tag_list: tags || [],
+ yaml_variables: yaml_variables,
+ options: {
+ image: image_value,
+ services: services_value,
+ before_script: before_script_value,
+ script: script_value || DEFAULT_SCRIPT
+ }.compact }
+ end
+
+ def yaml_variables
+ return unless variables_value
+
+ variables_value.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 795878b71f2..98cac0b0d1d 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -73,7 +73,7 @@ fi
# Switch to the gitlab path, exit on failure.
if ! cd "$app_root" ; then
- echo "Failed to cd into $app_root, exiting!"; exit 1
+ echo "Failed to cd into $app_root, exiting!"; exit 1
fi
if [ -z "$SIDEKIQ_WORKERS" ]; then
@@ -341,7 +341,7 @@ start_gitlab() {
echo "Gitaly is already running with pid $gapid, not restarting"
else
$app_root/bin/daemon_with_pidfile $gitaly_pid_path \
- $gitaly_dir/gitaly $gitaly_dir/config.toml >> $gitaly_log 2>&1 &
+ $gitaly_dir/gitaly $gitaly_dir/config.toml >> $gitaly_log 2>&1 &
fi
fi
@@ -413,39 +413,39 @@ print_status() {
return
fi
if [ "$web_status" = "0" ]; then
- echo "The GitLab web server with pid $wpid is running."
+ echo "The GitLab web server with pid $wpid is running."
else
- printf "The GitLab web server is \033[31mnot running\033[0m.\n"
+ printf "The GitLab web server is \033[31mnot running\033[0m.\n"
fi
if [ "$sidekiq_status" = "0" ]; then
- echo "The GitLab Sidekiq job dispatcher with pid $spid is running."
+ echo "The GitLab Sidekiq job dispatcher with pid $spid is running."
else
- printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n"
+ printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n"
fi
if [ "$gitlab_workhorse_status" = "0" ]; then
- echo "The GitLab Workhorse with pid $hpid is running."
+ echo "The GitLab Workhorse with pid $hpid is running."
else
- printf "The GitLab Workhorse is \033[31mnot running\033[0m.\n"
+ printf "The GitLab Workhorse is \033[31mnot running\033[0m.\n"
fi
if [ "$mail_room_enabled" = true ]; then
if [ "$mail_room_status" = "0" ]; then
- echo "The GitLab MailRoom email processor with pid $mpid is running."
+ echo "The GitLab MailRoom email processor with pid $mpid is running."
else
- printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
+ printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
if [ "$gitlab_pages_enabled" = true ]; then
if [ "$gitlab_pages_status" = "0" ]; then
- echo "The GitLab Pages with pid $gppid is running."
+ echo "The GitLab Pages with pid $gppid is running."
else
- printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
+ printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
fi
fi
if [ "$gitaly_enabled" = true ]; then
if [ "$gitaly_status" = "0" ]; then
- echo "Gitaly with pid $gapid is running."
+ echo "Gitaly with pid $gapid is running."
else
- printf "Gitaly is \033[31mnot running\033[0m.\n"
+ printf "Gitaly is \033[31mnot running\033[0m.\n"
fi
fi
if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; } && { [ "$gitaly_enabled" != true ] || [ "$gitaly_status" = "0" ]; }; then
@@ -490,25 +490,25 @@ restart_gitlab(){
case "$1" in
start)
- start_gitlab
- ;;
+ start_gitlab
+ ;;
stop)
- stop_gitlab
- ;;
+ stop_gitlab
+ ;;
restart)
- restart_gitlab
- ;;
+ restart_gitlab
+ ;;
reload|force-reload)
- reload_gitlab
- ;;
+ reload_gitlab
+ ;;
status)
- print_status
- exit $gitlab_status
- ;;
+ print_status
+ exit $gitlab_status
+ ;;
*)
- echo "Usage: service gitlab {start|stop|restart|reload|status}"
- exit 1
- ;;
+ echo "Usage: service gitlab {start|stop|restart|reload|status}"
+ exit 1
+ ;;
esac
exit
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8a9020dc0a5..75b4bef695c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4977,6 +4977,9 @@ msgstr ""
msgid "ClusterIntegration|No deployment cluster found for this job"
msgstr ""
+msgid "ClusterIntegration|No deployment found for this job"
+msgstr ""
+
msgid "ClusterIntegration|No instance type found"
msgstr ""
diff --git a/package.json b/package.json
index a0a078efb5d..29ad0a39348 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,8 @@
"@babel/plugin-syntax-import-meta": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@gitlab/at.js": "1.5.5",
- "@gitlab/svgs": "1.128.0",
- "@gitlab/ui": "14.14.2",
+ "@gitlab/svgs": "1.130.0",
+ "@gitlab/ui": "14.17.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3",
"@sentry/browser": "^5.10.2",
diff --git a/scripts/gemfile_lock_changed.sh b/scripts/gemfile_lock_changed.sh
index 24e2c685f11..eec31af1f77 100755
--- a/scripts/gemfile_lock_changed.sh
+++ b/scripts/gemfile_lock_changed.sh
@@ -2,7 +2,7 @@
gemfile_lock_changed() {
if [ -n "$(git diff --name-only -- Gemfile.lock)" ]; then
- cat << EOF
+ cat <<EOF
Gemfile was updated but Gemfile.lock was not updated.
Usually, when Gemfile is updated, you should run
diff --git a/scripts/lint-changelog-filenames b/scripts/lint-changelog-filenames
index 2355ac6f7b2..fc07b7153aa 100755
--- a/scripts/lint-changelog-filenames
+++ b/scripts/lint-changelog-filenames
@@ -6,7 +6,7 @@ lint_paths="changelogs/unreleased"
invalid_files=$(find $lint_paths -type f -not -name "*.yml" -not -name ".gitkeep")
if [ -n "$invalid_files" ]; then
- echo "Changelog files must end in .yml, but these did not:"
- echo "$invalid_files" | sed -e "s/^/* /"
+ echo "Changelog files must end in .yml, but these did not:"
+ echo "$invalid_files" | sed -e "s/^/* /"
exit 1
fi
diff --git a/scripts/sync-stable-branch.sh b/scripts/sync-stable-branch.sh
index 5aaec323628..59ab52844fb 100644
--- a/scripts/sync-stable-branch.sh
+++ b/scripts/sync-stable-branch.sh
@@ -7,56 +7,56 @@ set -e
if [[ "$MERGE_TRAIN_TRIGGER_TOKEN" == '' ]]
then
- echo 'The variable MERGE_TRAIN_TRIGGER_TOKEN must be set to a non-empty value'
- exit 1
+ echo 'The variable MERGE_TRAIN_TRIGGER_TOKEN must be set to a non-empty value'
+ exit 1
fi
if [[ "$MERGE_TRAIN_TRIGGER_URL" == '' ]]
then
- echo 'The variable MERGE_TRAIN_TRIGGER_URL must be set to a non-empty value'
- exit 1
+ echo 'The variable MERGE_TRAIN_TRIGGER_URL must be set to a non-empty value'
+ exit 1
fi
if [[ "$CI_COMMIT_REF_NAME" == '' ]]
then
- echo 'The variable CI_COMMIT_REF_NAME must be set to a non-empty value'
- exit 1
+ echo 'The variable CI_COMMIT_REF_NAME must be set to a non-empty value'
+ exit 1
fi
if [[ "$SOURCE_PROJECT" == '' ]]
then
- echo 'The variable SOURCE_PROJECT must be set to a non-empty value'
- exit 1
+ echo 'The variable SOURCE_PROJECT must be set to a non-empty value'
+ exit 1
fi
if [[ "$TARGET_PROJECT" == '' ]]
then
- echo 'The variable TARGET_PROJECT must be set to a non-empty value'
- exit 1
+ echo 'The variable TARGET_PROJECT must be set to a non-empty value'
+ exit 1
fi
if [[ "$TARGET_PROJECT" != "gitlab-org/gitlab-foss" ]]
then
- echo 'This is a security FOSS merge train'
- echo "Checking if $CI_COMMIT_SHA is available on canonical"
+ echo 'This is a security FOSS merge train'
+ echo "Checking if $CI_COMMIT_SHA is available on canonical"
- gitlab_com_commit_status=$(curl -s "https://gitlab.com/api/v4/projects/278964/repository/commits/$CI_COMMIT_SHA" | jq -M .status)
+ gitlab_com_commit_status=$(curl -s "https://gitlab.com/api/v4/projects/278964/repository/commits/$CI_COMMIT_SHA" | jq -M .status)
- if [[ "$gitlab_com_commit_status" != "null" ]]
- then
- echo 'Commit available on canonical, skipping merge train'
- exit 0
- fi
+ if [[ "$gitlab_com_commit_status" != "null" ]]
+ then
+ echo 'Commit available on canonical, skipping merge train'
+ exit 0
+ fi
- echo 'Commit not available, triggering a merge train'
+ echo 'Commit not available, triggering a merge train'
fi
curl -X POST \
- -F token="$MERGE_TRAIN_TRIGGER_TOKEN" \
- -F ref=master \
- -F "variables[MERGE_FOSS]=1" \
- -F "variables[SOURCE_BRANCH]=$CI_COMMIT_REF_NAME" \
- -F "variables[TARGET_BRANCH]=${CI_COMMIT_REF_NAME/-ee/}" \
- -F "variables[SOURCE_PROJECT]=$SOURCE_PROJECT" \
- -F "variables[TARGET_PROJECT]=$TARGET_PROJECT" \
- "$MERGE_TRAIN_TRIGGER_URL"
+ -F token="$MERGE_TRAIN_TRIGGER_TOKEN" \
+ -F ref=master \
+ -F "variables[MERGE_FOSS]=1" \
+ -F "variables[SOURCE_BRANCH]=$CI_COMMIT_REF_NAME" \
+ -F "variables[TARGET_BRANCH]=${CI_COMMIT_REF_NAME/-ee/}" \
+ -F "variables[SOURCE_PROJECT]=$SOURCE_PROJECT" \
+ -F "variables[TARGET_PROJECT]=$TARGET_PROJECT" \
+ "$MERGE_TRAIN_TRIGGER_URL"
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 897f8d5a8b8..f81e5c8982a 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -1,25 +1,25 @@
function retry() {
+ if eval "$@"; then
+ return 0
+ fi
+
+ for i in 2 1; do
+ sleep 3s
+ echo "Retrying $i..."
if eval "$@"; then
- return 0
+ return 0
fi
-
- for i in 2 1; do
- sleep 3s
- echo "Retrying $i..."
- if eval "$@"; then
- return 0
- fi
- done
- return 1
+ done
+ return 1
}
function setup_db_user_only() {
- source scripts/create_postgres_user.sh
+ source scripts/create_postgres_user.sh
}
function setup_db() {
- run_timed_command "setup_db_user_only"
- run_timed_command "bundle exec rake db:drop db:create db:structure:load db:migrate gitlab:db:setup_ee"
+ run_timed_command "setup_db_user_only"
+ run_timed_command "bundle exec rake db:drop db:create db:structure:load db:migrate gitlab:db:setup_ee"
}
function install_api_client_dependencies_with_apk() {
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index ef1253edda5..edf7fa0fb94 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1225,4 +1225,198 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
get :terminal_websocket_authorize, params: params.merge(extra_params)
end
end
+
+ describe 'GET #proxy_websocket_authorize' do
+ let_it_be(:owner) { create(:owner) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:project) { create(:project, :private, :repository, namespace: owner.namespace) }
+ let(:user) { maintainer }
+ let(:pipeline) { create(:ci_pipeline, project: project, source: :webide, config_source: :webide_source, user: user) }
+ let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline, user: user) }
+ let(:extra_params) { { id: job.id } }
+ let(:path) { :proxy_websocket_authorize }
+ let(:render_method) { :channel_websocket }
+ let(:expected_data) do
+ {
+ 'Channel' => {
+ 'Subprotocols' => ["terminal.gitlab.com"],
+ 'Url' => 'wss://localhost/proxy/build/default_port/',
+ 'Header' => {
+ 'Authorization' => [nil]
+ },
+ 'MaxSessionTime' => nil,
+ 'CAPem' => nil
+ }
+ }.to_json
+ end
+
+ before do
+ stub_feature_flags(build_service_proxy: true)
+ allow(job).to receive(:has_terminal?).and_return(true)
+
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+
+ sign_in(user)
+ end
+
+ context 'access rights' do
+ before do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+
+ make_request
+ end
+
+ context 'with admin' do
+ let(:user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with owner' do
+ let(:user) { owner }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with maintainer' do
+ let(:user) { maintainer }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with developer' do
+ let(:user) { developer }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with reporter' do
+ let(:user) { reporter }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with guest' do
+ let(:user) { guest }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with non member' do
+ let(:user) { create(:user) }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when pipeline is not from a webide source' do
+ context 'with admin' do
+ let(:user) { admin }
+ let(:pipeline) { create(:ci_pipeline, project: project, source: :chat, user: user) }
+
+ before do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+ make_request
+ end
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when workhorse signature is valid' do
+ before do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+ end
+
+ context 'and the id is valid' do
+ it 'returns the proxy data for the service running in the job' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(response.body).to eq(expected_data)
+ end
+ end
+
+ context 'and the id is invalid' do
+ let(:extra_params) { { id: non_existing_record_id } }
+
+ it 'returns 404' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with invalid workhorse signature' do
+ it 'aborts with an exception' do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError)
+
+ expect { make_request }.to raise_error(JWT::DecodeError)
+ end
+ end
+
+ context 'when feature flag :build_service_proxy is disabled' do
+ let(:user) { admin }
+
+ it 'returns 404' do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+ stub_feature_flags(build_service_proxy: false)
+
+ make_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it 'converts the url scheme into wss' do
+ allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
+
+ expect(job.runner_session_url).to start_with('https://')
+ expect(Gitlab::Workhorse).to receive(:channel_websocket).with(a_hash_including(url: "wss://localhost/proxy/build/default_port/"))
+
+ make_request
+ end
+
+ def make_request
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ get path, params: params.merge(extra_params)
+ end
+ end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index e648f83e57e..8db7a571c62 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -718,7 +718,7 @@ describe Projects::PipelinesController do
end
shared_examples 'creates a pipeline' do
- it do
+ specify do
expect { post_request }.to change { project.ci_pipelines.count }.by(1)
pipeline = project.ci_pipelines.last
diff --git a/spec/controllers/projects/web_ide_terminals_controller_spec.rb b/spec/controllers/projects/web_ide_terminals_controller_spec.rb
new file mode 100644
index 00000000000..6ccb9f84494
--- /dev/null
+++ b/spec/controllers/projects/web_ide_terminals_controller_spec.rb
@@ -0,0 +1,304 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::WebIdeTerminalsController do
+ let_it_be(:owner) { create(:owner) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:project) { create(:project, :private, :repository, namespace: owner.namespace) }
+ let(:pipeline) { create(:ci_pipeline, project: project, source: :webide, config_source: :webide_source, user: user) }
+ let(:job) { create(:ci_build, pipeline: pipeline, user: user, project: project) }
+ let(:user) { maintainer }
+
+ before do
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+
+ sign_in(user)
+ end
+
+ shared_examples 'terminal access rights' do
+ context 'with admin' do
+ let(:user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with owner' do
+ let(:user) { owner }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with maintainer' do
+ let(:user) { maintainer }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with developer' do
+ let(:user) { developer }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with reporter' do
+ let(:user) { reporter }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with guest' do
+ let(:user) { guest }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with non member' do
+ let(:user) { create(:user) }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ shared_examples 'when pipeline is not from a webide source' do
+ context 'with admin' do
+ let(:user) { admin }
+ let(:pipeline) { create(:ci_pipeline, project: project, source: :chat, user: user) }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET show' do
+ before do
+ get(:show, params: { namespace_id: project.namespace.to_param, project_id: project, id: job.id })
+ end
+
+ it_behaves_like 'terminal access rights'
+ it_behaves_like 'when pipeline is not from a webide source'
+ end
+
+ describe 'POST check_config' do
+ let(:result) { { status: :success } }
+
+ before do
+ allow_next_instance_of(::Ci::WebIdeConfigService) do |instance|
+ allow(instance).to receive(:execute).and_return(result)
+ end
+
+ post :check_config, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch: 'master'
+ }
+ end
+
+ it_behaves_like 'terminal access rights'
+
+ context 'when invalid config file' do
+ let(:user) { admin }
+ let(:result) { { status: :error } }
+
+ it 'returns 422', :enable_admin_mode do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+ end
+
+ describe 'POST create' do
+ let(:branch) { 'master' }
+
+ subject do
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch: branch
+ }
+ end
+
+ context 'when terminal job is created successfully' do
+ let(:build) { create(:ci_build, project: project) }
+ let(:pipeline) { build.pipeline }
+
+ before do
+ allow_next_instance_of(::Ci::CreateWebIdeTerminalService) do |instance|
+ allow(instance).to receive(:execute).and_return(status: :success, pipeline: pipeline)
+ end
+ end
+
+ context 'access rights' do
+ before do
+ subject
+ end
+
+ it_behaves_like 'terminal access rights'
+ end
+
+ it 'increases the web ide terminal counter' do
+ expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_terminals_count)
+
+ subject
+ end
+ end
+
+ shared_examples 'web ide terminal usage counter' do
+ it 'does not increase', :enable_admin_mode do
+ expect(Gitlab::UsageDataCounters::WebIdeCounter).not_to receive(:increment_terminals_count)
+
+ subject
+ end
+ end
+
+ context 'when branch does not exist' do
+ let(:user) { admin }
+ let(:branch) { 'foobar' }
+
+ it 'returns 400', :enable_admin_mode do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it_behaves_like 'web ide terminal usage counter'
+ end
+
+ context 'when there is an error creating the job' do
+ let(:user) { admin }
+
+ before do
+ allow_next_instance_of(::Ci::CreateWebIdeTerminalService) do |instance|
+ allow(instance).to receive(:execute).and_return(status: :error, message: 'foobar')
+ end
+ end
+
+ it 'returns 400', :enable_admin_mode do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it_behaves_like 'web ide terminal usage counter'
+ end
+
+ context 'when the current build is nil' do
+ let(:user) { admin }
+
+ before do
+ allow(pipeline).to receive(:builds).and_return([])
+ allow_next_instance_of(::Ci::CreateWebIdeTerminalService) do |instance|
+ allow(instance).to receive(:execute).and_return(status: :success, pipeline: pipeline)
+ end
+ end
+
+ it 'returns 400', :enable_admin_mode do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it_behaves_like 'web ide terminal usage counter'
+ end
+ end
+
+ describe 'POST cancel' do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline, user: user, project: project) }
+
+ before do
+ post(:cancel, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: job.id
+ })
+ end
+
+ it_behaves_like 'terminal access rights'
+ it_behaves_like 'when pipeline is not from a webide source'
+
+ context 'when job is not cancelable' do
+ let!(:job) { create(:ci_build, :failed, pipeline: pipeline, user: user) }
+
+ it 'returns 422' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+ end
+
+ describe 'POST retry' do
+ let(:status) { :failed }
+ let(:job) { create(:ci_build, status, pipeline: pipeline, user: user, project: project) }
+
+ before do
+ post(:retry, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: job.id
+ })
+ end
+
+ it_behaves_like 'terminal access rights'
+ it_behaves_like 'when pipeline is not from a webide source'
+
+ context 'when job is not retryable' do
+ let(:status) { :running }
+
+ it 'returns 422' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when job is cancelled' do
+ let(:status) { :canceled }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when job fails' do
+ let(:status) { :failed }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when job is successful' do
+ let(:status) { :success }
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 0b3653a01ed..eeb828d5914 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -155,6 +155,11 @@ FactoryBot.define do
source_sha { merge_request.source_branch_sha }
target_sha { merge_request.target_branch_sha }
end
+
+ trait :webide do
+ source { :webide }
+ config_source { :webide_source }
+ end
end
end
end
diff --git a/spec/features/groups/analytics/cycle_analytics_spec.rb b/spec/features/groups/analytics/cycle_analytics_spec.rb
new file mode 100644
index 00000000000..54be861dfb4
--- /dev/null
+++ b/spec/features/groups/analytics/cycle_analytics_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Group value stream analytics' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ RSpec::Matchers.define :have_pushed_frontend_feature_flags do |expected|
+ def to_js(key, value)
+ "\"#{key}\":#{value}"
+ end
+
+ match do |actual|
+ expected.all? do |feature_flag_name, enabled|
+ page.html.include?(to_js(feature_flag_name, enabled))
+ end
+ end
+
+ failure_message do |actual|
+ missing = expected.select do |feature_flag_name, enabled|
+ !page.html.include?(to_js(feature_flag_name, enabled))
+ end
+
+ formatted_missing_flags = missing.map { |feature_flag_name, enabled| to_js(feature_flag_name, enabled) }.join("\n")
+
+ "The following feature flag(s) cannot be found in the frontend HTML source: #{formatted_missing_flags}"
+ end
+ end
+
+ before do
+ stub_licensed_features(cycle_analytics_for_groups: true)
+
+ group.add_owner(user)
+
+ sign_in(user)
+ end
+
+ it 'pushes frontend feature flags' do
+ visit group_analytics_cycle_analytics_path(group)
+
+ expect(page).to have_pushed_frontend_feature_flags(
+ cycleAnalyticsScatterplotEnabled: true,
+ cycleAnalyticsScatterplotMedianEnabled: true,
+ valueStreamAnalyticsPathNavigation: true
+ )
+ end
+
+ context 'when `value_stream_analytics_path_navigation` is disabled for a group' do
+ before do
+ stub_feature_flags(value_stream_analytics_path_navigation: false, thing: group)
+ end
+
+ it 'pushes disabled feature flag to the frontend' do
+ visit group_analytics_cycle_analytics_path(group)
+
+ expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false)
+ end
+ end
+end
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index 834a053eec9..0f18600a736 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -38,6 +38,7 @@ describe('AlertManagementList', () => {
const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
+ const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
const alertsCount = {
acknowledged: 6,
@@ -80,7 +81,10 @@ describe('AlertManagementList', () => {
});
}
+ const mockStartedAtCol = {};
+
beforeEach(() => {
+ jest.spyOn(document, 'querySelector').mockReturnValue(mockStartedAtCol);
mountComponent();
});
@@ -284,6 +288,34 @@ describe('AlertManagementList', () => {
});
});
+ describe('sorting the alert list by column', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false, sort: 'START_TIME_ASC', alertsCount },
+ loading: false,
+ });
+ });
+
+ it('updates sort with new direction and column key', () => {
+ findSeverityColumnHeader().trigger('click');
+
+ expect(wrapper.vm.$data.sort).toEqual('SEVERITY_ASC');
+
+ findSeverityColumnHeader().trigger('click');
+
+ expect(wrapper.vm.$data.sort).toEqual('SEVERITY_DESC');
+ });
+
+ it('updates the `ariaSort` attribute so the sort icon appears in the proper column', () => {
+ expect(mockStartedAtCol.ariaSort).toEqual('ascending');
+
+ findSeverityColumnHeader().trigger('click');
+
+ expect(mockStartedAtCol.ariaSort).toEqual('none');
+ });
+ });
+
describe('updating the alert status', () => {
const iid = '1527542';
const mockUpdatedMutationResult = {
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index e2d810d5ddc..526a5589743 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -154,9 +154,11 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
.to receive(:fetch_remote)
.with('github', forced: false)
- expect(Rails.logger)
- .to receive(:info)
- .with(an_instance_of(String))
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:info)
+ .with(an_instance_of(Hash))
+ end
expect(importer.repository_updates_counter)
.to receive(:increment)
diff --git a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
new file mode 100644
index 00000000000..d6217811b9c
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AttributesPermitter do
+ let(:yml_config) do
+ <<-EOF
+ tree:
+ project:
+ - labels:
+ - :priorities
+ - milestones:
+ - events:
+ - :push_event_payload
+
+ included_attributes:
+ labels:
+ - :title
+ - :description
+
+ methods:
+ labels:
+ - :type
+ EOF
+ end
+
+ let(:file) { Tempfile.new(%w(import_export .yml)) }
+ let(:config_hash) { Gitlab::ImportExport::Config.new(config: file.path).to_h }
+
+ before do
+ file.write(yml_config)
+ file.rewind
+ end
+
+ after do
+ file.close
+ file.unlink
+ end
+
+ subject { described_class.new(config: config_hash) }
+
+ describe '#permitted_attributes' do
+ it 'builds permitted attributes hash' do
+ expect(subject.permitted_attributes).to match(
+ a_hash_including(
+ project: [:labels, :milestones],
+ labels: [:priorities, :title, :description, :type],
+ events: [:push_event_payload],
+ milestones: [:events],
+ priorities: [],
+ push_event_payload: []
+ )
+ )
+ end
+ end
+
+ describe '#permit' do
+ let(:unfiltered_hash) do
+ {
+ title: 'Title',
+ description: 'Description',
+ undesired_attribute: 'Undesired Attribute',
+ another_attribute: 'Another Attribute'
+ }
+ end
+
+ it 'only allows permitted attributes' do
+ expect(subject.permit(:labels, unfiltered_hash)).to eq(title: 'Title', description: 'Description')
+ end
+ end
+
+ describe '#permitted_attributes_for' do
+ it 'returns an array of permitted attributes for a relation' do
+ expect(subject.permitted_attributes_for(:labels)).to contain_exactly(:title, :description, :type, :priorities)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
new file mode 100644
index 00000000000..04b0752c6fe
--- /dev/null
+++ b/spec/lib/gitlab/web_ide/config/entry/global_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::WebIde::Config::Entry::Global do
+ let(:global) { described_class.new(hash) }
+
+ describe '.nodes' do
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a(Hash)
+ end
+
+ context 'when filtering all the entry/node names' do
+ it 'contains the expected node names' do
+ expect(described_class.nodes.keys)
+ .to match_array(%i[terminal])
+ end
+ end
+ end
+
+ context 'when configuration is valid' do
+ context 'when some entries defined' do
+ let(:hash) do
+ { terminal: { before_script: ['ls'], variables: {}, script: 'sleep 10s', services: ['mysql'] } }
+ end
+
+ describe '#compose!' do
+ before do
+ global.compose!
+ end
+
+ it 'creates nodes hash' do
+ expect(global.descendants).to be_an Array
+ end
+
+ it 'creates node object for each entry' do
+ expect(global.descendants.count).to eq 1
+ end
+
+ it 'creates node object using valid class' do
+ expect(global.descendants.first)
+ .to be_an_instance_of Gitlab::WebIde::Config::Entry::Terminal
+ end
+
+ it 'sets correct description for nodes' do
+ expect(global.descendants.first.description)
+ .to eq 'Configuration of the webide terminal.'
+ end
+
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
+ end
+ end
+
+ context 'when not composed' do
+ describe '#terminal_value' do
+ it 'returns nil' do
+ expect(global.terminal_value).to be nil
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is leaf' do
+ expect(global).to be_leaf
+ end
+ end
+ end
+
+ context 'when composed' do
+ before do
+ global.compose!
+ end
+
+ describe '#errors' do
+ it 'has no errors' do
+ expect(global.errors).to be_empty
+ end
+ end
+
+ describe '#terminal_value' do
+ it 'returns correct script' do
+ expect(global.terminal_value).to eq({
+ tag_list: [],
+ yaml_variables: [],
+ options: {
+ before_script: ['ls'],
+ script: ['sleep 10s'],
+ services: [{ name: "mysql" }]
+ }
+ })
+ end
+ end
+ end
+ end
+ end
+
+ context 'when configuration is not valid' do
+ before do
+ global.compose!
+ end
+
+ context 'when job does not have valid before script' do
+ let(:hash) do
+ { terminal: { before_script: 100 } }
+ end
+
+ describe '#errors' do
+ it 'reports errors about missing script' do
+ expect(global.errors)
+ .to include "terminal:before_script config should be an array containing strings and arrays of strings"
+ end
+ end
+ end
+ end
+
+ context 'when value is not a hash' do
+ let(:hash) { [] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'returns error about invalid type' do
+ expect(global.errors.first).to match /should be a hash/
+ end
+ end
+ end
+
+ describe '#specified?' do
+ it 'is concrete entry that is defined' do
+ expect(global.specified?).to be true
+ end
+ end
+
+ describe '#[]' do
+ before do
+ global.compose!
+ end
+
+ let(:hash) do
+ { terminal: { before_script: ['ls'] } }
+ end
+
+ context 'when entry exists' do
+ it 'returns correct entry' do
+ expect(global[:terminal])
+ .to be_an_instance_of Gitlab::WebIde::Config::Entry::Terminal
+ expect(global[:terminal][:before_script].value).to eq ['ls']
+ end
+ end
+
+ context 'when entry does not exist' do
+ it 'always return unspecified node' do
+ expect(global[:some][:unknown][:node])
+ .not_to be_specified
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
new file mode 100644
index 00000000000..882e389e040
--- /dev/null
+++ b/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::WebIde::Config::Entry::Terminal do
+ let(:entry) { described_class.new(config, with_image_ports: true) }
+
+ describe '.nodes' do
+ context 'when filtering all the entry/node names' do
+ subject { described_class.nodes.keys }
+
+ let(:result) do
+ %i[before_script script image services variables]
+ end
+
+ it { is_expected.to match_array result }
+ end
+ end
+
+ describe 'validations' do
+ before do
+ entry.compose!
+ end
+
+ context 'when entry config value is correct' do
+ let(:config) { { script: 'rspec' } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when the same port is not duplicated' do
+ let(:config) do
+ {
+ image: { name: "ruby", ports: [80] },
+ services: [{ name: "mysql", alias: "service1", ports: [81] }, { name: "mysql", alias: "service2", ports: [82] }]
+ }
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when unknown port keys detected' do
+ let(:config) do
+ {
+ image: { name: "ruby", ports: [80] },
+ services: [{ name: "mysql", alias: "service2", ports: [{ number: 81, invalid_key: 'foobar' }] }]
+ }
+ end
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors.first)
+ .to match /port config contains unknown keys: invalid_key/
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ context 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'reports error about a config type' do
+ expect(entry.errors)
+ .to include 'terminal config should be a hash'
+ end
+ end
+ end
+
+ context 'when config is empty' do
+ let(:config) { {} }
+
+ describe '#valid' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when unknown keys detected' do
+ let(:config) { { unknown: true } }
+
+ describe '#valid' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when the same port is duplicated' do
+ let(:config) do
+ {
+ image: { name: "ruby", ports: [80] },
+ services: [{ name: "mysql", ports: [80] }, { name: "mysql", ports: [81] }]
+ }
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors.count).to eq 1
+ expect(entry.errors.first).to match "each port number can only be referenced once"
+ end
+ end
+ end
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is a relevant entry' do
+ entry = described_class.new({ script: 'rspec' })
+
+ expect(entry).to be_relevant
+ end
+ end
+
+ context 'when composed' do
+ before do
+ entry.compose!
+ end
+
+ describe '#value' do
+ context 'when entry is correct' do
+ let(:config) do
+ { before_script: %w[ls pwd],
+ script: 'sleep 100',
+ tags: ['webide'],
+ image: 'ruby:2.5',
+ services: ['mysql'],
+ variables: { KEY: 'value' } }
+ end
+
+ it 'returns correct value' do
+ expect(entry.value)
+ .to eq(
+ tag_list: ['webide'],
+ yaml_variables: [{ key: 'KEY', value: 'value', public: true }],
+ options: {
+ image: { name: "ruby:2.5" },
+ services: [{ name: "mysql" }],
+ before_script: %w[ls pwd],
+ script: ['sleep 100']
+ }
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/web_ide/config_spec.rb b/spec/lib/gitlab/web_ide/config_spec.rb
new file mode 100644
index 00000000000..c1dafd01197
--- /dev/null
+++ b/spec/lib/gitlab/web_ide/config_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::WebIde::Config do
+ let(:config) do
+ described_class.new(yml)
+ end
+
+ context 'when config is valid' do
+ let(:yml) do
+ <<-EOS
+ terminal:
+ image: ruby:2.7
+ before_script:
+ - gem install rspec
+ EOS
+ end
+
+ describe '#to_hash' do
+ it 'returns hash created from string' do
+ hash = {
+ terminal: {
+ image: 'ruby:2.7',
+ before_script: ['gem install rspec']
+ }
+ }
+
+ expect(config.to_hash).to eq hash
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'has no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when config is invalid' do
+ context 'when yml is incorrect' do
+ let(:yml) { '// invalid' }
+
+ describe '.new' do
+ it 'raises error' do
+ expect { config }.to raise_error(
+ described_class::ConfigError,
+ /Invalid configuration format/
+ )
+ end
+ end
+ end
+
+ context 'when config logic is incorrect' do
+ let(:yml) { 'terminal: { before_script: "ls" }' }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'has errors' do
+ expect(config.errors).not_to be_empty
+ end
+ end
+
+ describe '#errors' do
+ it 'returns an array of strings' do
+ expect(config.errors).to all(be_an_instance_of(String))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index cdf56f24cd7..3e520407884 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -63,4 +63,64 @@ describe Ci::BuildRunnerSession, model: true do
end
end
end
+
+ describe '#service_specification' do
+ let(:service) { 'foo'}
+ let(:port) { 80 }
+ let(:path) { 'path' }
+ let(:subprotocols) { nil }
+ let(:specification) { subject.service_specification(service: service, port: port, path: path, subprotocols: subprotocols) }
+
+ it 'returns service proxy url' do
+ expect(specification[:url]).to eq "https://localhost/proxy/#{service}/#{port}/#{path}"
+ end
+
+ it 'returns default service proxy websocket subprotocol' do
+ expect(specification[:subprotocols]).to eq %w[terminal.gitlab.com]
+ end
+
+ it 'returns empty hash if no url' do
+ subject.url = ''
+
+ expect(specification).to be_empty
+ end
+
+ context 'when port is not present' do
+ let(:port) { nil }
+
+ it 'uses the default port name' do
+ expect(specification[:url]).to eq "https://localhost/proxy/#{service}/default_port/#{path}"
+ end
+ end
+
+ context 'when the service is not present' do
+ let(:service) { '' }
+
+ it 'uses the service name "build" as default' do
+ expect(specification[:url]).to eq "https://localhost/proxy/build/#{port}/#{path}"
+ end
+ end
+
+ context 'when url is present' do
+ it 'returns ca_pem nil if empty certificate' do
+ subject.certificate = ''
+
+ expect(specification[:ca_pem]).to be_nil
+ end
+
+ it 'adds Authorization header if authorization is present' do
+ subject.authorization = 'foobar'
+
+ expect(specification[:headers]).to include(Authorization: ['foobar'])
+ end
+ end
+
+ context 'when subprotocol is present' do
+ let(:subprotocols) { 'foobar' }
+
+ it 'returns the new subprotocol' do
+ expect(specification[:subprotocols]).to eq [subprotocols]
+ end
+ end
+ end
end
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index 0b6b858daa8..f3b83c036b5 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -62,11 +62,16 @@ describe ProjectImportState, type: :model do
it 'logs error when update column fails' do
allow(import_state).to receive(:update_column).and_raise(ActiveRecord::ActiveRecordError)
- allow(Gitlab::AppLogger).to receive(:error)
- import_state.mark_as_failed(error_message)
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ error: 'ActiveRecord::ActiveRecordError',
+ message: 'Error setting import status to failed',
+ original_error: error_message
+ )
+ end
- expect(Gitlab::AppLogger).to have_received(:error)
+ import_state.mark_as_failed(error_message)
end
it 'updates last_error with error message' do
diff --git a/spec/models/web_ide_terminal_spec.rb b/spec/models/web_ide_terminal_spec.rb
new file mode 100644
index 00000000000..4103a26c75a
--- /dev/null
+++ b/spec/models/web_ide_terminal_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe WebIdeTerminal do
+ let(:build) { create(:ci_build) }
+
+ subject { described_class.new(build) }
+
+ it 'returns the show_path of the build' do
+ expect(subject.show_path).to end_with("/ide_terminals/#{build.id}")
+ end
+
+ it 'returns the retry_path of the build' do
+ expect(subject.retry_path).to end_with("/ide_terminals/#{build.id}/retry")
+ end
+
+ it 'returns the cancel_path of the build' do
+ expect(subject.cancel_path).to end_with("/ide_terminals/#{build.id}/cancel")
+ end
+
+ it 'returns the terminal_path of the build' do
+ expect(subject.terminal_path).to end_with("/jobs/#{build.id}/terminal.ws")
+ end
+
+ it 'returns the proxy_websocket_path of the build' do
+ expect(subject.proxy_websocket_path).to end_with("/jobs/#{build.id}/proxy.ws")
+ end
+
+ describe 'services' do
+ let(:services_with_aliases) do
+ {
+ services: [{ name: 'postgres', alias: 'postgres' },
+ { name: 'docker:stable-dind', alias: 'docker' }]
+ }
+ end
+
+ before do
+ allow(build).to receive(:options).and_return(config)
+ end
+
+ context 'when image does not have an alias' do
+ let(:config) do
+ { image: 'ruby:2.7' }.merge(services_with_aliases)
+ end
+
+ it 'returns services aliases' do
+ expect(subject.services).to eq %w(postgres docker)
+ end
+ end
+
+ context 'when both image and services have aliases' do
+ let(:config) do
+ { image: { name: 'ruby:2.7', alias: 'ruby' } }.merge(services_with_aliases)
+ end
+
+ it 'returns all aliases' do
+ expect(subject.services).to eq %w(postgres docker ruby)
+ end
+ end
+
+ context 'when image and services does not have any alias' do
+ let(:config) do
+ { image: 'ruby:2.7', services: ['postgres'] }
+ end
+
+ it 'returns an empty array' do
+ expect(subject.services).to be_empty
+ end
+ end
+
+ context 'when no image nor services' do
+ let(:config) do
+ { script: %w(echo) }
+ end
+
+ it 'returns an empty array' do
+ expect(subject.services).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index f29ed26f2aa..5857369a550 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -249,4 +249,129 @@ describe Ci::BuildPolicy do
end
end
end
+
+ describe 'manage a web ide terminal' do
+ let(:build_permissions) { %i[read_web_ide_terminal create_build_terminal update_web_ide_terminal create_build_service_proxy] }
+ let_it_be(:maintainer) { create(:user) }
+ let(:owner) { create(:owner) }
+ let(:admin) { create(:admin) }
+ let(:maintainer) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:project) { create(:project, :public, namespace: owner.namespace) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, source: :webide) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ allow(build).to receive(:has_terminal?).and_return(true)
+
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+ end
+
+ subject { described_class.new(current_user, build) }
+
+ context 'when create_web_ide_terminal access enabled' do
+ context 'with admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode enabled', :enable_admin_mode do
+ it { expect_allowed(*build_permissions) }
+ end
+
+ context 'when admin mode disabled' do
+ it { expect_disallowed(*build_permissions) }
+ end
+
+ context 'when build is not from a webide pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, source: :chat) }
+
+ it { expect_disallowed(:read_web_ide_terminal, :update_web_ide_terminal, :create_build_service_proxy) }
+ end
+
+ context 'when build has no runner terminal' do
+ before do
+ allow(build).to receive(:has_terminal?).and_return(false)
+ end
+
+ context 'when admin mode enabled', :enable_admin_mode do
+ it { expect_allowed(:read_web_ide_terminal, :update_web_ide_terminal) }
+ it { expect_disallowed(:create_build_terminal, :create_build_service_proxy) }
+ end
+
+ context 'when admin mode disabled' do
+ it { expect_disallowed(:read_web_ide_terminal, :update_web_ide_terminal) }
+ it { expect_disallowed(:create_build_terminal, :create_build_service_proxy) }
+ end
+ end
+
+ context 'feature flag "build_service_proxy" is disabled' do
+ before do
+ stub_feature_flags(build_service_proxy: false)
+ end
+
+ it { expect_disallowed(:create_build_service_proxy) }
+ end
+ end
+
+ shared_examples 'allowed build owner access' do
+ it { expect_disallowed(*build_permissions) }
+
+ context 'when user is the owner of the job' do
+ let(:build) { create(:ci_build, pipeline: pipeline, user: current_user) }
+
+ it { expect_allowed(*build_permissions) }
+ end
+ end
+
+ shared_examples 'forbidden access' do
+ it { expect_disallowed(*build_permissions) }
+
+ context 'when user is the owner of the job' do
+ let(:build) { create(:ci_build, pipeline: pipeline, user: current_user) }
+
+ it { expect_disallowed(*build_permissions) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it_behaves_like 'allowed build owner access'
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it_behaves_like 'allowed build owner access'
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'forbidden access'
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'forbidden access'
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'forbidden access'
+ end
+
+ context 'with non member' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'forbidden access'
+ end
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 7c98d8cc856..0e2234d7299 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -742,4 +742,62 @@ describe ProjectPolicy do
it { is_expected.to be_disallowed(:destroy_package) }
end
end
+
+ describe 'create_web_ide_terminal' do
+ subject { described_class.new(current_user, project) }
+
+ context 'with admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:create_web_ide_terminal) }
+ end
+
+ context 'when admin mode disabled' do
+ it { is_expected.to be_disallowed(:create_web_ide_terminal) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_web_ide_terminal) }
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_web_ide_terminal) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_web_ide_terminal) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_web_ide_terminal) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:create_web_ide_terminal) }
+ end
+
+ context 'with non member' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_disallowed(:create_web_ide_terminal) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_web_ide_terminal) }
+ end
+ end
end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index db8932ab63a..62a4d3b48b2 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
describe API::Issues do
let_it_be(:user) { create(:user) }
let_it_be(:owner) { create(:owner) }
- let_it_be(:project, reload: true) do
- create(:project, :public, creator_id: owner.id, namespace: owner.namespace)
- end
-
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
@@ -17,6 +13,11 @@ describe API::Issues do
let(:admin) { create(:user, :admin) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
+
+ let_it_be(:project, reload: true) do
+ create(:project, :public, creator_id: owner.id, namespace: owner.namespace)
+ end
+
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -28,6 +29,7 @@ describe API::Issues do
updated_at: 3.hours.ago,
closed_at: 1.hour.ago
end
+
let!(:confidential_issue) do
create :issue,
:confidential,
@@ -37,6 +39,7 @@ describe API::Issues do
created_at: generate(:past_time),
updated_at: 2.hours.ago
end
+
let!(:issue) do
create :issue,
author: user,
@@ -48,18 +51,24 @@ describe API::Issues do
title: issue_title,
description: issue_description
end
+
let_it_be(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
+
let!(:label_link) { create(:label_link, label: label, target: issue) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+
let_it_be(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project)
end
- let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { 'None' }
let(:any_milestone_title) { 'Any' }
+ let(:updated_title) { 'updated title' }
+ let(:issue_path) { "/projects/#{project.id}/issues/#{issue.iid}" }
+ let(:api_for_user) { api(issue_path, user) }
before_all do
project.add_reporter(user)
@@ -72,108 +81,97 @@ describe API::Issues do
describe 'PUT /projects/:id/issues/:issue_iid to update only title' do
it 'updates a project issue' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(:ok)
+ put api_for_user, params: { title: updated_title }
- expect(json_response['title']).to eq('updated title')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['title']).to eq(updated_title)
end
it 'returns 404 error if issue iid not found' do
- put api("/projects/#{project.id}/issues/44444", user),
- params: { title: 'updated title' }
+ put api("/projects/#{project.id}/issues/44444", user), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 404 error if issue id is used instead of the iid' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
- params: { title: 'updated title' }
+ put api("/projects/#{project.id}/issues/#{issue.id}", user), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ put api_for_user,
params: {
- title: 'updated title',
+ title: updated_title,
labels: 'label, label?, label&foo, ?, &'
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
end
it 'allows special label names with labels param as array' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ put api_for_user,
params: {
- title: 'updated title',
+ title: updated_title,
labels: ['label', 'label?', 'label&foo, ?, &']
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
+ expect(json_response['labels']).to contain_exactly('label', 'label?', 'label&foo', '?', '&')
end
context 'confidential issues' do
+ let(:confidential_issue_path) { "/projects/#{project.id}/issues/#{confidential_issue.iid}" }
+
it 'returns 403 for non project members' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
- params: { title: 'updated title' }
+ put api(confidential_issue_path, non_member), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns 403 for project members with guest role' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
- params: { title: 'updated title' }
+ put api(confidential_issue_path, guest), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'updates a confidential issue for project members' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { title: 'updated title' }
+ put api(confidential_issue_path, user), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['title']).to eq('updated title')
+ expect(json_response['title']).to eq(updated_title)
end
it 'updates a confidential issue for author' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
- params: { title: 'updated title' }
+ put api(confidential_issue_path, author), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['title']).to eq('updated title')
+ expect(json_response['title']).to eq(updated_title)
end
it 'updates a confidential issue for admin' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
- params: { title: 'updated title' }
+ put api(confidential_issue_path, admin), params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['title']).to eq('updated title')
+ expect(json_response['title']).to eq(updated_title)
end
it 'sets an issue to confidential' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { confidential: true }
+ put api_for_user, params: { confidential: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be_truthy
end
it 'makes a confidential issue public' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { confidential: false }
+ put api(confidential_issue_path, user), params: { confidential: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be_falsy
end
it 'does not update a confidential issue with wrong confidential flag' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { confidential: 'foo' }
+ put api(confidential_issue_path, user), params: { confidential: 'foo' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('confidential is invalid')
@@ -185,12 +183,12 @@ describe API::Issues do
include_context 'includes Spam constants'
def update_issue
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
+ put api_for_user, params: params
end
let(:params) do
{
- title: 'updated title',
+ title: updated_title,
description: 'content here',
labels: 'label, label2'
}
@@ -224,7 +222,7 @@ describe API::Issues do
it 'creates a new spam log entry' do
expect { update_issue }
- .to log_spam(title: 'updated title', description: 'content here', user_id: user.id, noteable_type: 'Issue')
+ .to log_spam(title: updated_title, description: 'content here', user_id: user.id, noteable_type: 'Issue')
end
end
@@ -241,7 +239,7 @@ describe API::Issues do
it 'creates a new spam log entry' do
expect { update_issue }
- .to log_spam(title: 'updated title', description: 'content here', user_id: user.id, noteable_type: 'Issue')
+ .to log_spam(title: updated_title, description: 'content here', user_id: user.id, noteable_type: 'Issue')
end
end
end
@@ -249,49 +247,39 @@ describe API::Issues do
describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
context 'support for deprecated assignee_id' do
it 'removes assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_id: 0 }
+ put api_for_user, params: { assignee_id: 0 }
expect(response).to have_gitlab_http_status(:ok)
-
expect(json_response['assignee']).to be_nil
end
it 'updates an issue with new assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_id: user2.id }
+ put api_for_user, params: { assignee_id: user2.id }
expect(response).to have_gitlab_http_status(:ok)
-
expect(json_response['assignee']['name']).to eq(user2.name)
end
end
it 'removes assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [0] }
+ put api_for_user, params: { assignee_ids: [0] }
expect(response).to have_gitlab_http_status(:ok)
-
expect(json_response['assignees']).to be_empty
end
it 'updates an issue with new assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [user2.id] }
+ put api_for_user, params: { assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(:ok)
-
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
context 'single assignee restrictions' do
it 'updates an issue with several assignees but only one has been applied' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [user2.id, guest.id] }
+ put api_for_user, params: { assignee_ids: [user2.id, guest.id] }
expect(response).to have_gitlab_http_status(:ok)
-
expect(json_response['assignees'].size).to eq(1)
end
end
@@ -302,8 +290,7 @@ describe API::Issues do
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'adds relevant labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { add_labels: '1, 2' }
+ put api_for_user, params: { add_labels: '1, 2' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly(label.title, '1', '2')
@@ -314,16 +301,14 @@ describe API::Issues do
let!(:label_link2) { create(:label_link, label: label2, target: issue) }
it 'removes relevant labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { remove_labels: label2.title }
+ put api_for_user, params: { remove_labels: label2.title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
it 'removes all labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { remove_labels: "#{label.title}, #{label2.title}" }
+ put api_for_user, params: { remove_labels: "#{label.title}, #{label2.title}" }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to be_empty
@@ -331,15 +316,15 @@ describe API::Issues do
end
it 'does not update labels if not present' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'updated title' }
+ put api_for_user, params: { title: updated_title }
+
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
it 'removes all labels and touches the record' do
Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
+ put api_for_user, params: { labels: '' }
end
expect(response).to have_gitlab_http_status(:ok)
@@ -349,7 +334,7 @@ describe API::Issues do
it 'removes all labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
+ put api_for_user, params: { labels: [''] }
end
expect(response).to have_gitlab_http_status(:ok)
@@ -359,20 +344,19 @@ describe API::Issues do
it 'updates labels and touches the record' do
Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'foo,bar' }
+ put api_for_user, params: { labels: 'foo,bar' }
end
+
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['labels']).to include 'foo'
- expect(json_response['labels']).to include 'bar'
+ expect(json_response['labels']).to contain_exactly('foo', 'bar')
expect(json_response['updated_at']).to be > Time.now
end
it 'updates labels and touches the record with labels param as array' do
Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: %w(foo bar) }
+ put api_for_user, params: { labels: %w(foo bar) }
end
+
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
@@ -380,36 +364,22 @@ describe API::Issues do
end
it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
+ put api_for_user, params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
+
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['labels']).to include 'label:foo'
- expect(json_response['labels']).to include 'label-bar'
- expect(json_response['labels']).to include 'label_bar'
- expect(json_response['labels']).to include 'label/bar'
- expect(json_response['labels']).to include 'label?bar'
- expect(json_response['labels']).to include 'label&bar'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
+ expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&')
end
it 'allows special label names with labels param as array' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
+ put api_for_user, params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
+
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['labels']).to include 'label:foo'
- expect(json_response['labels']).to include 'label-bar'
- expect(json_response['labels']).to include 'label_bar'
- expect(json_response['labels']).to include 'label/bar'
- expect(json_response['labels']).to include 'label?bar'
- expect(json_response['labels']).to include 'label&bar'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
+ expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&')
end
it 'returns 400 if title is too long' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'g' * 256 }
+ put api_for_user, params: { title: 'g' * 256 }
+
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
@@ -419,16 +389,15 @@ describe API::Issues do
describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do
it 'updates a project issue' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label2', state_event: 'close' }
- expect(response).to have_gitlab_http_status(:ok)
+ put api_for_user, params: { labels: 'label2', state_event: 'close' }
- expect(json_response['labels']).to include 'label2'
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['labels']).to contain_exactly('label2')
expect(json_response['state']).to eq 'closed'
end
it 'reopens a project isssue' do
- put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
+ put api(issue_path, user), params: { state_event: 'reopen' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['state']).to eq 'opened'
@@ -440,42 +409,41 @@ describe API::Issues do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'some new title', updated_at: update_time }
+ put api_for_user, params: { title: 'some new title', updated_at: update_time }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['title']).to include 'some new title'
+ expect(json_response['title']).to eq('some new title')
expect(Time.parse(json_response['updated_at'])).not_to be_like_time(update_time)
end
end
context 'when admin or owner makes the request' do
+ let(:api_for_owner) { api(issue_path, owner) }
+
it 'not allow to set null for updated_at' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", owner), params: { updated_at: nil }
+ put api_for_owner, params: { updated_at: nil }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'not allow to set blank for updated_at' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", owner), params: { updated_at: '' }
+ put api_for_owner, params: { updated_at: '' }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'not allow to set invalid format for updated_at' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", owner), params: { updated_at: 'invalid-format' }
+ put api_for_owner, params: { updated_at: 'invalid-format' }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
- put api("/projects/#{project.id}/issues/#{issue.iid}", owner),
- params: { title: 'some new title', updated_at: update_time }
+ put api_for_owner, params: { title: 'some new title', updated_at: update_time }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['title']).to include 'some new title'
-
+ expect(json_response['title']).to eq('some new title')
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
end
@@ -485,7 +453,7 @@ describe API::Issues do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
+ put api_for_user, params: { due_date: due_date }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['due_date']).to eq(due_date)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index a4331c3a0ec..6359140c426 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1055,6 +1055,65 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent }
end
end
+
+ context 'for web-ide job' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
+ let(:service) { Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute }
+ let(:pipeline) { service[:pipeline] }
+ let(:build) { pipeline.builds.first }
+ let(:job) { {} }
+ let(:config_content) do
+ 'terminal: { image: ruby, services: [mysql], before_script: [ls], tags: [tag-1], variables: { KEY: value } }'
+ end
+
+ before do
+ stub_webide_config_file(config_content)
+ project.add_maintainer(user)
+
+ pipeline
+ end
+
+ context 'when runner has matching tag' do
+ before do
+ runner.update!(tag_list: ['tag-1'])
+ end
+
+ it 'successfully picks job' do
+ request_job
+
+ build.reload
+
+ expect(build).to be_running
+ expect(build.runner).to eq(runner)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include(
+ "id" => build.id,
+ "variables" => include("key" => 'KEY', "value" => 'value', "public" => true, "masked" => false),
+ "image" => a_hash_including("name" => 'ruby'),
+ "services" => all(a_hash_including("name" => 'mysql')),
+ "job_info" => a_hash_including("name" => 'terminal', "stage" => 'terminal'))
+ end
+ end
+
+ context 'when runner does not have matching tags' do
+ it 'does not pick a job' do
+ request_job
+
+ build.reload
+
+ expect(build).to be_pending
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ def request_job(token = runner.token, **params)
+ post api('/jobs/request'), params: params.merge(token: token)
+ end
+ end
end
describe 'PUT /api/v4/jobs/:id' do
diff --git a/spec/serializers/web_ide_terminal_entity_spec.rb b/spec/serializers/web_ide_terminal_entity_spec.rb
new file mode 100644
index 00000000000..e163afa14ed
--- /dev/null
+++ b/spec/serializers/web_ide_terminal_entity_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe WebIdeTerminalEntity do
+ let(:build) { create(:ci_build) }
+ let(:entity) { described_class.new(WebIdeTerminal.new(build)) }
+
+ subject { entity.as_json }
+
+ it { is_expected.to have_key(:id) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.to have_key(:show_path) }
+ it { is_expected.to have_key(:cancel_path) }
+ it { is_expected.to have_key(:retry_path) }
+ it { is_expected.to have_key(:terminal_path) }
+ it { is_expected.to have_key(:services) }
+ it { is_expected.to have_key(:proxy_websocket_path) }
+
+ context 'when feature flag build_service_proxy is disabled' do
+ before do
+ stub_feature_flags(build_service_proxy: false)
+ end
+
+ it { is_expected.not_to have_key(:proxy_websocket_path) }
+ end
+end
diff --git a/spec/serializers/web_ide_terminal_serializer_spec.rb b/spec/serializers/web_ide_terminal_serializer_spec.rb
new file mode 100644
index 00000000000..01133deaf84
--- /dev/null
+++ b/spec/serializers/web_ide_terminal_serializer_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe WebIdeTerminalSerializer do
+ let(:build) { create(:ci_build) }
+
+ subject { described_class.new.represent(WebIdeTerminal.new(build)) }
+
+ it 'represents WebIdeTerminalEntity entities' do
+ expect(described_class.entity_class).to eq(WebIdeTerminalEntity)
+ end
+
+ it 'accepts WebIdeTerminal as a resource' do
+ expect(subject[:id]).to eq build.id
+ end
+
+ context 'when resource is a build' do
+ subject { described_class.new.represent(build) }
+
+ it 'transforms it into a WebIdeTerminal resource' do
+ expect(WebIdeTerminal).to receive(:new)
+
+ subject
+ end
+ end
+end
diff --git a/spec/services/ci/create_web_ide_terminal_service_spec.rb b/spec/services/ci/create_web_ide_terminal_service_spec.rb
new file mode 100644
index 00000000000..2cc67c7cd1d
--- /dev/null
+++ b/spec/services/ci/create_web_ide_terminal_service_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::CreateWebIdeTerminalService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let(:ref) { 'master' }
+
+ describe '#execute' do
+ subject { described_class.new(project, user, ref: ref).execute }
+
+ context 'for maintainer' do
+ shared_examples 'be successful' do
+ it 'returns a success with pipeline object' do
+ is_expected.to include(status: :success)
+
+ expect(subject[:pipeline]).to be_a(Ci::Pipeline)
+ expect(subject[:pipeline]).to be_persisted
+ expect(subject[:pipeline].stages.count).to eq(1)
+ expect(subject[:pipeline].builds.count).to eq(1)
+ end
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when web-ide has valid configuration' do
+ before do
+ stub_webide_config_file(config_content)
+ end
+
+ context 'for empty configuration' do
+ let(:config_content) do
+ 'terminal: {}'
+ end
+
+ it_behaves_like 'be successful'
+ end
+
+ context 'for configuration with container image' do
+ let(:config_content) do
+ 'terminal: { image: ruby }'
+ end
+
+ it_behaves_like 'be successful'
+ end
+
+ context 'for configuration with ports' do
+ let(:config_content) do
+ <<-EOS
+ terminal:
+ image:
+ name: ruby:2.7
+ ports:
+ - 80
+ script: rspec
+ services:
+ - name: test
+ alias: test
+ ports:
+ - 8080
+ EOS
+ end
+
+ it_behaves_like 'be successful'
+ end
+ end
+ end
+
+ context 'error handling' do
+ shared_examples 'having an error' do |message|
+ it 'returns an error' do
+ is_expected.to eq(
+ status: :error,
+ message: message
+ )
+ end
+ end
+
+ shared_examples 'having insufficient permissions' do
+ it_behaves_like 'having an error', 'Insufficient permissions to create a terminal'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'having insufficient permissions'
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when terminal is already running' do
+ let!(:webide_pipeline) { create(:ci_pipeline, :webide, :running, project: project, user: user) }
+
+ it_behaves_like 'having an error', 'There is already a terminal running'
+ end
+
+ context 'when ref is non-existing' do
+ let(:ref) { 'non-existing-ref' }
+
+ it_behaves_like 'having an error', 'Ref does not exist'
+ end
+
+ context 'when ref is a tag' do
+ let(:ref) { 'v1.0.0' }
+
+ it_behaves_like 'having an error', 'Ref needs to be a branch'
+ end
+
+ context 'when terminal config is missing' do
+ let(:ref) { 'v1.0.0' }
+
+ it_behaves_like 'having an error', 'Ref needs to be a branch'
+ end
+
+ context 'when webide config is present' do
+ before do
+ stub_webide_config_file(config_content)
+ end
+
+ context 'config has invalid content' do
+ let(:config_content) { 'invalid' }
+
+ it_behaves_like 'having an error', 'Invalid configuration format'
+ end
+
+ context 'config is valid, but does not have terminal' do
+ let(:config_content) { '{}' }
+
+ it_behaves_like 'having an error', 'Terminal is not configured'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/web_ide_config_service_spec.rb b/spec/services/ci/web_ide_config_service_spec.rb
new file mode 100644
index 00000000000..7522103ccb7
--- /dev/null
+++ b/spec/services/ci/web_ide_config_service_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::WebIdeConfigService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let(:sha) { 'sha' }
+
+ describe '#execute' do
+ subject { described_class.new(project, user, sha: sha).execute }
+
+ context 'when insufficient permission' do
+ it 'returns an error' do
+ is_expected.to include(
+ status: :error,
+ message: 'Insufficient permissions to read configuration')
+ end
+ end
+
+ context 'for developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when file is missing' do
+ it 'returns an error' do
+ is_expected.to include(
+ status: :error,
+ message: "Failed to load Web IDE config file '.gitlab/.gitlab-webide.yml' for sha")
+ end
+ end
+
+ context 'when file is present' do
+ before do
+ allow(project.repository).to receive(:blob_data_at).with('sha', anything) do
+ config_content
+ end
+ end
+
+ context 'content is not valid' do
+ let(:config_content) { 'invalid content' }
+
+ it 'returns an error' do
+ is_expected.to include(
+ status: :error,
+ message: "Invalid configuration format")
+ end
+ end
+
+ context 'content is valid, but terminal not defined' do
+ let(:config_content) { '{}' }
+
+ it 'returns success' do
+ is_expected.to include(
+ status: :success,
+ terminal: nil)
+ end
+ end
+
+ context 'content is valid, with enabled terminal' do
+ let(:config_content) { 'terminal: {}' }
+
+ it 'returns success' do
+ is_expected.to include(
+ status: :success,
+ terminal: {
+ tag_list: [],
+ yaml_variables: [],
+ options: { script: ["sleep 60"] }
+ })
+ end
+ end
+
+ context 'content is valid, with custom terminal' do
+ let(:config_content) { 'terminal: { before_script: [ls] }' }
+
+ it 'returns success' do
+ is_expected.to include(
+ status: :success,
+ terminal: {
+ tag_list: [],
+ yaml_variables: [],
+ options: { before_script: ["ls"], script: ["sleep 60"] }
+ })
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
index f14c929554a..bb0b107eba6 100644
--- a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
+++ b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
@@ -85,13 +85,25 @@ describe Clusters::ParseClusterApplicationsArtifactService do
end
end
- context 'job has no deployment cluster' do
+ context 'job has no deployment' do
let(:job) { build(:ci_build) }
it 'returns an error' do
result = described_class.new(job, user).execute(artifact)
expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('No deployment found for this job')
+ end
+ end
+
+ context 'job has no deployment cluster' do
+ let(:deployment) { create(:deployment) }
+ let(:job) { deployment.deployable }
+
+ it 'returns an error' do
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No deployment cluster found for this job')
end
end
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index 7bad68b4e00..4576c786416 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -103,12 +103,14 @@ describe Groups::ImportExport::ExportService do
end
it 'logs the error' do
- expect(shared.logger).to receive(:error).with(
- group_id: group.id,
- group_name: group.name,
- error: expected_message,
- message: 'Group Import/Export: Export failed'
- )
+ expect_next_instance_of(Gitlab::Export::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ group_id: group.id,
+ group_name: group.name,
+ errors: expected_message,
+ message: 'Group Export failed'
+ )
+ end
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
end
@@ -162,7 +164,8 @@ describe Groups::ImportExport::ExportService do
it 'notifies logger' do
allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false)
- expect(shared.logger).to receive(:error)
+
+ expect(service.instance_variable_get(:@logger)).to receive(:error)
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
end
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index f7e8171f1cf..19891341311 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -119,9 +119,7 @@ describe Projects::ImportExport::ExportService do
end
it 'notifies logger' do
- allow(Gitlab::AppLogger).to receive(:error)
-
- expect(Gitlab::AppLogger).to receive(:error)
+ expect(service.instance_variable_get(:@logger)).to receive(:error)
end
end
end
@@ -149,7 +147,7 @@ describe Projects::ImportExport::ExportService do
end
it 'notifies logger' do
- expect(Gitlab::AppLogger).to receive(:error)
+ expect(service.instance_variable_get(:@logger)).to receive(:error)
end
it 'does not call the export strategy' do
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 120d432655b..4da8f760056 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -141,6 +141,12 @@ module StubGitlabCalls
.to_return(status: 200, body: "", headers: {})
end
+ def stub_webide_config_file(content, sha: anything)
+ allow_any_instance_of(Repository)
+ .to receive(:blob_data_at).with(sha, '.gitlab/.gitlab-webide.yml')
+ .and_return(content)
+ end
+
def project_hash_array
f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
Gitlab::Json.parse(f)
diff --git a/yarn.lock b/yarn.lock
index a56c0f61371..d51db915315 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -782,15 +782,15 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
-"@gitlab/svgs@1.128.0":
- version "1.128.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.128.0.tgz#c510050d5646d73b52e684248a186dbd1f55cbb0"
- integrity sha512-RqgF6k2xPptbz58RB1nNgeo6gy3l1u7+1rxXvALzIAsazmrAw708NYCT3PALg2RoyH0G/fpUa6yPQ0HbR+OtEg==
-
-"@gitlab/ui@14.14.2":
- version "14.14.2"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.14.2.tgz#7cc81d90d5b5394345d6781ff02e974e24b97387"
- integrity sha512-Fq7fGjhofnN64xckTuuuX4EE23ZXcndwCfFBFrCTCbDfrDSa0l0xkmkrvYCSrNNTp6CyL5Ec/LWgGcnGCPWaFw==
+"@gitlab/svgs@1.130.0":
+ version "1.130.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.130.0.tgz#0c2f3cdc0a4b0f54c47b2861c8fa31b2a58c570a"
+ integrity sha512-azJ1E9PBk6fGOaP6816BSr8oYrQu3m3BbYZwWOCUp8AfbZuf0ZOZVYmlR9i/eAOhoqqqmwF8hYCK2VjAklbpPA==
+
+"@gitlab/ui@14.17.0":
+ version "14.17.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.17.0.tgz#668f08318e9ef4d36a05c1ec13d531a8e46983b4"
+ integrity sha512-p75/lFQ0w5Mlg0DMdF2g/LW8DTJoSua4Xoh9BQO80o+Kw3ALQFOvvZAx17AW/MbgRD775I7Yv3V84H+s/xUqwg==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"