diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-25 18:07:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-25 18:07:58 +0300 |
commit | 0d8e625e4cd499162e6113dca4988b28f9faa9b6 (patch) | |
tree | 8744ab9fdba76e924f15272bba473521e39b8760 /app | |
parent | b5249f2d99206a72459bc5e2bf2aeb2f06ee36f3 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
28 files changed, 585 insertions, 52 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) |