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 | |
parent | b5249f2d99206a72459bc5e2bf2aeb2f06ee36f3 (diff) |
Add latest changes from gitlab-org/gitlab@master
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) @@ -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 Binary files differdeleted file mode 100644 index 7c6157e38e0..00000000000 --- a/doc/install/aws/img/rds_subnet_group.png +++ /dev/null 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" |