diff options
48 files changed, 457 insertions, 365 deletions
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index 0b98f9c0101..9b1c45a3a49 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -43,17 +43,17 @@ export default { key: 'environment_scope', label: __('Environment scope'), }, - // Wait for backend to send these fields - // { - // key: 'size', - // label: __('Size'), - // }, + { + key: 'node_size', + label: __('Nodes'), + }, + // Fields are missing calculation methods and not ready to display // { - // key: 'cpu', + // key: 'node_cpu', // label: __('Total cores (vCPUs)'), // }, // { - // key: 'memory', + // key: 'node_memory', // label: __('Total memory (GB)'), // }, { @@ -111,6 +111,14 @@ export default { ></div> </div> </template> + + <template #cell(node_size)="{ item }"> + <span v-if="item.nodes">{{ item.nodes.length }}</span> + <small v-else class="gl-font-sm gl-font-style-italic gl-text-gray-400">{{ + __('Unknown') + }}</small> + </template> + <template #cell(cluster_type)="{value}"> <gl-badge variant="light"> {{ value }} diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index eebcaa086f9..077bf0b8925 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -6,6 +6,8 @@ export const CLUSTER_TYPES = { instance_type: __('Instance'), }; +export const MAX_REQUESTS = 3; + export const STATUSES = { default: { className: 'bg-white', title: __('Unknown') }, disabled: { className: 'disabled', title: __('Disabled') }, diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js index 919625f69b4..5245c307c8c 100644 --- a/app/assets/javascripts/clusters_list/store/actions.js +++ b/app/assets/javascripts/clusters_list/store/actions.js @@ -2,10 +2,23 @@ import Poll from '~/lib/utils/poll'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; +import { MAX_REQUESTS } from '../constants'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import * as Sentry from '@sentry/browser'; import * as types from './mutation_types'; +const allNodesPresent = (clusters, retryCount) => { + /* + Nodes are coming from external Kubernetes clusters. + They may fail for reasons GitLab cannot control. + MAX_REQUESTS will ensure this poll stops at some point. + */ + return retryCount > MAX_REQUESTS || clusters.every(cluster => cluster.nodes != null); +}; + export const fetchClusters = ({ state, commit }) => { + let retryCount = 0; + const poll = new Poll({ resource: { fetchClusters: paginatedEndPoint => axios.get(paginatedEndPoint), @@ -13,16 +26,40 @@ export const fetchClusters = ({ state, commit }) => { data: `${state.endpoint}?page=${state.page}`, method: 'fetchClusters', successCallback: ({ data, headers }) => { - if (data.clusters) { - const normalizedHeaders = normalizeHeaders(headers); - const paginationInformation = parseIntPagination(normalizedHeaders); + retryCount += 1; + + try { + if (data.clusters) { + const normalizedHeaders = normalizeHeaders(headers); + const paginationInformation = parseIntPagination(normalizedHeaders); + + commit(types.SET_CLUSTERS_DATA, { data, paginationInformation }); + commit(types.SET_LOADING_STATE, false); - commit(types.SET_CLUSTERS_DATA, { data, paginationInformation }); - commit(types.SET_LOADING_STATE, false); + if (allNodesPresent(data.clusters, retryCount)) { + poll.stop(); + } + } + } catch (error) { poll.stop(); + + Sentry.withScope(scope => { + scope.setTag('javascript_clusters_list', 'fetchClustersSuccessCallback'); + Sentry.captureException(error); + }); } }, - errorCallback: () => flash(__('An error occurred while loading clusters')), + errorCallback: response => { + poll.stop(); + + commit(types.SET_LOADING_STATE, false); + flash(__('Clusters|An error occurred while loading clusters')); + + Sentry.withScope(scope => { + scope.setTag('javascript_clusters_list', 'fetchClustersErrorCallback'); + Sentry.captureException(response); + }); + }, }); poll.makeRequest(); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index a070cf8866a..9b911f99c83 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -412,7 +412,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" </gl-alert> <div class="note-form-actions"> <div - class="float-left btn-group + class="btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <button diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue index dff21d919a9..fa142385f06 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_area.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue @@ -47,7 +47,7 @@ export default { }; </script> <template> - <div class="d-flex flex-grow-1 flex-column"> + <div class="d-flex flex-grow-1 flex-column h-100"> <edit-header class="py-2" :title="title" /> <rich-content-editor v-model="editableContent" class="mb-9" /> <publish-toolbar diff --git a/app/assets/stylesheets/pages/storage_quota.scss b/app/assets/stylesheets/pages/storage_quota.scss index 97ae4f0ade4..347bd1316c0 100644 --- a/app/assets/stylesheets/pages/storage_quota.scss +++ b/app/assets/stylesheets/pages/storage_quota.scss @@ -9,14 +9,8 @@ @include gl-rounded-bottom-right-base; } - &:not(:first-child) { - @include gl-border-l-1; - @include gl-border-l-solid; - @include gl-border-white; - } - &:not(:last-child) { - @include gl-border-r-1; + @include gl-border-r-2; @include gl-border-r-solid; @include gl-border-white; } diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index aa39d430b24..46dec5f3287 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -23,6 +23,7 @@ class Clusters::ClustersController < Clusters::BaseController respond_to do |format| format.html format.json do + Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL) serializer = ClusterSerializer.new(current_user: current_user) render json: { diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 5aa00af8910..9ef067e8797 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -5,7 +5,6 @@ module IssuableCollections include PaginatedCollection include SortingHelper include SortingPreference - include Gitlab::IssuableMetadata include Gitlab::Utils::StrongMemoize included do @@ -44,7 +43,7 @@ module IssuableCollections def set_pagination @issuables = @issuables.page(params[:page]) @issuables = per_page_for_relative_position if params[:sort] == 'relative_position' - @issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user) + @issuable_meta_data = Gitlab::IssuableMetadata.new(current_user, @issuables).data @total_pages = issuable_page_count(@issuables) end # rubocop:enable Gitlab/ModuleWithInstanceVariables diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb index 78b3c6771b3..e3ac117660b 100644 --- a/app/controllers/concerns/issuable_collections_action.rb +++ b/app/controllers/concerns/issuable_collections_action.rb @@ -11,7 +11,7 @@ module IssuableCollectionsAction .non_archived .page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues, collection_type, current_user) + @issuable_meta_data = Gitlab::IssuableMetadata.new(current_user, @issues).data respond_to do |format| format.html @@ -22,7 +22,7 @@ module IssuableCollectionsAction def merge_requests @merge_requests = issuables_collection.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type, current_user) + @issuable_meta_data = Gitlab::IssuableMetadata.new(current_user, @merge_requests).data end # rubocop:enable Gitlab/ModuleWithInstanceVariables diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ff292973546..3597a0f307a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -314,8 +314,7 @@ class ProjectsController < Projects::ApplicationController @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) @issues = issuables_collection.page(params[:page]) - @collection_type = 'Issue' - @issuable_meta_data = issuable_meta_data(@issues, @collection_type, current_user) + @issuable_meta_data = Gitlab::IssuableMetadata.new(current_user, @issues).data end render :show diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 30ea383bd73..6f12ec921d4 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,10 +23,17 @@ module Ci project_type: 3 } - ONLINE_CONTACT_TIMEOUT = 1.hour + # This `ONLINE_CONTACT_TIMEOUT` needs to be larger than + # `RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY` + # + ONLINE_CONTACT_TIMEOUT = 2.hours + + # The `RUNNER_QUEUE_EXPIRY_TIME` indicates the longest interval that + # Runner request needs to be refreshed by Rails instead of being handled + # by Workhorse RUNNER_QUEUE_EXPIRY_TIME = 1.hour - # This needs to be less than `ONLINE_CONTACT_TIMEOUT` + # The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze @@ -282,7 +289,7 @@ module Ci ensure_runner_queue_value == value if value.present? end - def update_cached_info(values) + def heartbeat(values) values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {} values[:contacted_at] = Time.current diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 5622a53bb5d..b202a6579e7 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -189,8 +189,10 @@ class CommitStatus < ApplicationRecord end def self.update_as_processed! - # Marks items as processed, and increases `lock_version` (Optimisitc Locking) - update_all('processed=TRUE, lock_version=COALESCE(lock_version,0)+1') + # Marks items as processed + # we do not increase `lock_version`, as we are the one + # holding given lock_version (Optimisitc Locking) + update_all(processed: true) end def self.locking_enabled? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f011426c9db..92d89ad9092 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -39,15 +39,6 @@ module Issuable locked: 4 }.with_indifferent_access.freeze - # This object is used to gather issuable meta data for displaying - # upvotes, downvotes, notes and closing merge requests count for issues and merge requests - # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do - def merge_requests_count(user = nil) - mrs_count - end - end - included do cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description, issuable_state_filter_enabled: true diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 7bebaca684a..59389a0fa65 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -2,8 +2,6 @@ module Projects class UpdatePagesService < BaseService - include Gitlab::OptimisticLocking - InvalidStateError = Class.new(StandardError) FailedToExtractError = Class.new(StandardError) @@ -25,8 +23,8 @@ module Projects # Create status notifying the deployment of pages @status = create_status - retry_optimistic_lock(@status, &:enqueue!) - retry_optimistic_lock(@status, &:run!) + @status.enqueue! + @status.run! raise InvalidStateError, 'missing pages artifacts' unless build.artifacts? raise InvalidStateError, 'build SHA is outdated for this ref' unless latest? @@ -53,7 +51,7 @@ module Projects private def success - retry_optimistic_lock(@status, &:success) + @status.success @project.mark_pages_as_deployed super end @@ -63,7 +61,7 @@ module Projects log_error("Projects::UpdatePagesService: #{message}") @status.allow_failure = !latest? @status.description = message - retry_optimistic_lock(@status) { |status| status.drop(:script_failure) } + @status.drop(:script_failure) super end diff --git a/changelogs/unreleased/196544-nodemetrics-size.yml b/changelogs/unreleased/196544-nodemetrics-size.yml new file mode 100644 index 00000000000..1319ef2f624 --- /dev/null +++ b/changelogs/unreleased/196544-nodemetrics-size.yml @@ -0,0 +1,5 @@ +--- +title: Added node size to cluster index +merge_request: 32435 +author: +type: changed diff --git a/changelogs/unreleased/219145-comment-button-does-not-show-up-on-mr-when-comments-are-set-to-new.yml b/changelogs/unreleased/219145-comment-button-does-not-show-up-on-mr-when-comments-are-set-to-new.yml new file mode 100644 index 00000000000..232842c3c5a --- /dev/null +++ b/changelogs/unreleased/219145-comment-button-does-not-show-up-on-mr-when-comments-are-set-to-new.yml @@ -0,0 +1,5 @@ +--- +title: Fix overflow issue in MR and Issue comments +merge_request: 33100 +author: +type: fixed diff --git a/changelogs/unreleased/fix-atomic-processing-lock-version.yml b/changelogs/unreleased/fix-atomic-processing-lock-version.yml new file mode 100644 index 00000000000..9db891d651f --- /dev/null +++ b/changelogs/unreleased/fix-atomic-processing-lock-version.yml @@ -0,0 +1,5 @@ +--- +title: Fix atomic processing bumping a lock_version +merge_request: 32914 +author: +type: fixed diff --git a/changelogs/unreleased/fix-runner-hearbeat.yml b/changelogs/unreleased/fix-runner-hearbeat.yml new file mode 100644 index 00000000000..8125bad48ab --- /dev/null +++ b/changelogs/unreleased/fix-runner-hearbeat.yml @@ -0,0 +1,5 @@ +--- +title: Fix Runner heartbeats that results in considering them offline +merge_request: 32851 +author: +type: fixed diff --git a/changelogs/unreleased/iterations_add_daterange_constraint.yml b/changelogs/unreleased/iterations_add_daterange_constraint.yml deleted file mode 100644 index bbc46cbe1e7..00000000000 --- a/changelogs/unreleased/iterations_add_daterange_constraint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add btree_gist PGSQL extension and add DB constraints for Iteration date ranges -merge_request: 32335 -author: -type: added diff --git a/db/migrate/20200515152649_enable_btree_gist_extension.rb b/db/migrate/20200515152649_enable_btree_gist_extension.rb deleted file mode 100644 index 686b685fb5d..00000000000 --- a/db/migrate/20200515152649_enable_btree_gist_extension.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class EnableBtreeGistExtension < ActiveRecord::Migration[6.0] - DOWNTIME = false - - def up - execute 'CREATE EXTENSION IF NOT EXISTS btree_gist' - end - - def down - execute 'DROP EXTENSION IF EXISTS btree_gist' - end -end diff --git a/db/migrate/20200515153633_iteration_date_range_constraint.rb b/db/migrate/20200515153633_iteration_date_range_constraint.rb deleted file mode 100644 index ab197ff8ae7..00000000000 --- a/db/migrate/20200515153633_iteration_date_range_constraint.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class IterationDateRangeConstraint < ActiveRecord::Migration[6.0] - DOWNTIME = false - - def up - execute <<~SQL - ALTER TABLE sprints - ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint - EXCLUDE USING gist - ( project_id WITH =, - daterange(start_date, due_date, '[]') WITH && - ) - WHERE (project_id IS NOT NULL) - SQL - - execute <<~SQL - ALTER TABLE sprints - ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint - EXCLUDE USING gist - ( group_id WITH =, - daterange(start_date, due_date, '[]') WITH && - ) - WHERE (group_id IS NOT NULL) - SQL - end - - def down - execute <<~SQL - ALTER TABLE sprints - DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_project_id_constraint - SQL - - execute <<~SQL - ALTER TABLE sprints - DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_group_id_constraint - SQL - end -end diff --git a/db/structure.sql b/db/structure.sql index 386296a53c4..e30b51c576c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2,8 +2,6 @@ SET search_path=public; CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; -CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public; - CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; CREATE TABLE public.abuse_reports ( @@ -8427,12 +8425,6 @@ ALTER TABLE ONLY public.issue_user_mentions ALTER TABLE ONLY public.issues ADD CONSTRAINT issues_pkey PRIMARY KEY (id); -ALTER TABLE ONLY public.sprints - ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint EXCLUDE USING gist (group_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL)); - -ALTER TABLE ONLY public.sprints - ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint EXCLUDE USING gist (project_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((project_id IS NOT NULL)); - ALTER TABLE ONLY public.jira_connect_installations ADD CONSTRAINT jira_connect_installations_pkey PRIMARY KEY (id); @@ -13953,8 +13945,6 @@ COPY "schema_migrations" (version) FROM STDIN; 20200514000009 20200514000132 20200514000340 -20200515152649 -20200515153633 20200515155620 20200519115908 20200519171058 diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 32ea461f4e9..4e10f1657b7 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -367,6 +367,8 @@ migration involves one of the high-traffic tables: - `users` - `projects` - `namespaces` +- `issues` +- `merge_requests` - `ci_pipelines` - `ci_builds` - `notes` diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index e7fbe392726..18e1d34f9ca 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -59,8 +59,6 @@ Here's a list of the AWS services we will use, with links to pricing information Redis configuration. See the [Amazon ElastiCache pricing](https://aws.amazon.com/elasticache/pricing/). -NOTE: **Note:** Please note that while we will be using EBS for storage, we do not recommend using EFS as it may negatively impact GitLab's performance. You can review the [relevant documentation](../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details. - ## Create an IAM EC2 instance role and profile As we'll be using [Amazon S3 object storage](#amazon-s3-object-storage), our EC2 instances need to have read, write, and list permissions for our S3 buckets. To avoid embedding AWS keys in our GitLab config, we'll make use of an [IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) to allow our GitLab instance with this access. We'll need to create an IAM policy to attach to our IAM role: @@ -563,7 +561,7 @@ Let's create an EC2 instance where we'll install Gitaly: 1. Click **Review and launch** followed by **Launch** if you're happy with your settings. 1. Finally, acknowledge that you have access to the selected private key file or create a new one. Click **Launch Instances**. - > **Optional:** Instead of storing configuration _and_ repository data on the root volume, you can also choose to add an additional EBS volume for repository storage. Follow the same guidance as above. See the [Amazon EBS pricing](https://aws.amazon.com/ebs/pricing/). +NOTE: **Optional:** Instead of storing configuration _and_ repository data on the root volume, you can also choose to add an additional EBS volume for repository storage. Follow the same guidance as above. See the [Amazon EBS pricing](https://aws.amazon.com/ebs/pricing/). We do not recommend using EFS as it may negatively impact GitLab’s performance. You can review the [relevant documentation](../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details. Now that we have our EC2 instance ready, follow the [documentation to install GitLab and set up Gitaly on its own server](../../administration/gitaly/index.md#running-gitaly-on-its-own-server). Perform the client setup steps from that document on the [GitLab instance we created](#install-gitlab) above. diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 42c4671a47c..9bacfcafbc6 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -563,6 +563,10 @@ If more than the maximum number of allowed connections occur concurrently, they dropped and users get [an `ssh_exchange_identification` error](../../topics/git/troubleshooting_git.md#ssh_exchange_identification-error). +### Import/export + +To help avoid abuse, project and group imports, exports, and export downloads are rate limited. See [Project import/export rate limits](../../user/project/settings/import_export.md#rate-limits) and [Group import/export rate limits](../../user/group/settings/import_export.md#rate-limits) for details. + ## GitLab.com Logging We use [Fluentd](https://gitlab.com/gitlab-com/runbooks/tree/master/logging/doc#fluentd) to parse our logs. Fluentd sends our logs to diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index fd09e2d4ee6..b4c5d7869a2 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -106,7 +106,7 @@ module API status.enqueue! when 'running' status.enqueue - Gitlab::OptimisticLocking.retry_lock(status, &:run!) + status.run! when 'success' status.success! when 'failed' diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 1f1253c8542..293d7ed9a6a 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -3,6 +3,8 @@ module API module Helpers module Runner + include Gitlab::Utils::StrongMemoize + prepend_if_ee('EE::API::Helpers::Runner') # rubocop: disable Cop/InjectEnterpriseEditionModule JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN' @@ -16,7 +18,7 @@ module API forbidden! unless current_runner current_runner - .update_cached_info(get_runner_details_from_request) + .heartbeat(get_runner_details_from_request) end def get_runner_details_from_request @@ -31,31 +33,35 @@ module API end def current_runner - @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) + strong_memoize(:current_runner) do + ::Ci::Runner.find_by_token(params[:token].to_s) + end end - def validate_job!(job) - not_found! unless job + def authenticate_job!(require_running: true) + job = current_job - yield if block_given? + not_found! unless job + forbidden! unless job_token_valid?(job) - project = job.project - forbidden!('Project has been deleted!') if project.nil? || project.pending_delete? + forbidden!('Project has been deleted!') if job.project.nil? || job.project.pending_delete? forbidden!('Job has been erased!') if job.erased? - end - def authenticate_job! - job = current_job + if require_running + job_forbidden!(job, 'Job is not running') unless job.running? + end - validate_job!(job) do - forbidden! unless job_token_valid?(job) + if Gitlab::Ci::Features.job_heartbeats_runner?(job.project) + job.runner&.heartbeat(get_runner_ip) end job end def current_job - @current_job ||= Ci::Build.find_by_id(params[:id]) + strong_memoize(:current_job) do + Ci::Build.find_by_id(params[:id]) + end end def job_token_valid?(job) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index de2d0b01a64..2374ac11f4a 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -5,7 +5,6 @@ module API include PaginationParams helpers Helpers::IssuesHelpers helpers Helpers::RateLimiter - helpers ::Gitlab::IssuableMetadata before { authenticate_non_get! } @@ -108,7 +107,7 @@ module API with: Entities::Issue, with_labels_details: declared_params[:with_labels_details], current_user: current_user, - issuable_metadata: issuable_meta_data(issues, 'Issue', current_user), + issuable_metadata: Gitlab::IssuableMetadata.new(current_user, issues).data, include_subscribed: false } @@ -134,7 +133,7 @@ module API with: Entities::Issue, with_labels_details: declared_params[:with_labels_details], current_user: current_user, - issuable_metadata: issuable_meta_data(issues, 'Issue', current_user), + issuable_metadata: Gitlab::IssuableMetadata.new(current_user, issues).data, include_subscribed: false, group: user_group } @@ -171,7 +170,7 @@ module API with_labels_details: declared_params[:with_labels_details], current_user: current_user, project: user_project, - issuable_metadata: issuable_meta_data(issues, 'Issue', current_user), + issuable_metadata: Gitlab::IssuableMetadata.new(current_user, issues).data, include_subscribed: false } diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index ff4ad85115b..32e4059f054 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -8,7 +8,6 @@ module API before { authenticate_non_get! } - helpers ::Gitlab::IssuableMetadata helpers Helpers::MergeRequestsHelpers # EE::API::MergeRequests would override the following helpers @@ -92,7 +91,7 @@ module API if params[:view] == 'simple' options[:with] = Entities::MergeRequestSimple else - options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user) + options[:issuable_metadata] = Gitlab::IssuableMetadata.new(current_user, merge_requests).data if Feature.enabled?(:mr_list_api_skip_merge_status_recheck, default_enabled: true) options[:skip_merge_status_recheck] = !declared_params[:with_merge_status_recheck] end diff --git a/lib/api/runner.rb b/lib/api/runner.rb index e070e57a376..5f08ebe4a06 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -154,7 +154,6 @@ module API end put '/:id' do job = authenticate_job! - job_forbidden!(job, 'Job is not running') unless job.running? job.trace.set(params[:trace]) if params[:trace] @@ -182,7 +181,6 @@ module API end patch '/:id/trace' do job = authenticate_job! - job_forbidden!(job, 'Job is not running') unless job.running? error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range') content_range = request.headers['Content-Range'] @@ -229,7 +227,6 @@ module API Gitlab::Workhorse.verify_api_request!(headers) job = authenticate_job! - forbidden!('Job is not running') unless job.running? service = Ci::AuthorizeJobArtifactService.new(job, params, max_size: max_artifacts_size(job)) @@ -265,7 +262,6 @@ module API require_gitlab_workhorse! job = authenticate_job! - forbidden!('Job is not running!') unless job.running? artifacts = params[:file] metadata = params[:metadata] @@ -292,7 +288,7 @@ module API optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts) end get '/:id/artifacts' do - job = authenticate_job! + job = authenticate_job!(require_running: false) present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download]) end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 02b8bb55274..43ed9c96486 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -6,8 +6,6 @@ module API before { authenticate! } - helpers ::Gitlab::IssuableMetadata - ISSUABLE_TYPES = { 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) }, 'issues' => ->(iid) { find_project_issue(iid) } @@ -65,7 +63,7 @@ module API next unless collection targets = collection.map(&:target) - options[type] = { issuable_metadata: issuable_meta_data(targets, type, current_user) } + options[type] = { issuable_metadata: Gitlab::IssuableMetadata.new(current_user, targets).data } end end end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 48f3d4fdd2f..06db38d1d7b 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -13,6 +13,10 @@ module Gitlab def self.ensure_scheduling_type_enabled? ::Feature.enabled?(:ci_ensure_scheduling_type, default_enabled: true) end + + def self.job_heartbeats_runner?(project) + ::Feature.enabled?(:ci_job_heartbeats_runner, project, default_enabled: true) + end end end end diff --git a/lib/gitlab/issuable_metadata.rb b/lib/gitlab/issuable_metadata.rb index 6f760751b0f..e946fc00c4d 100644 --- a/lib/gitlab/issuable_metadata.rb +++ b/lib/gitlab/issuable_metadata.rb @@ -1,8 +1,52 @@ # frozen_string_literal: true module Gitlab - module IssuableMetadata - def issuable_meta_data(issuable_collection, collection_type, user = nil) + class IssuableMetadata + include Gitlab::Utils::StrongMemoize + + # data structure to store issuable meta data like + # upvotes, downvotes, notes and closing merge requests counts for issues and merge requests + # this avoiding n+1 queries when loading issuable collections on frontend + IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :mrs_count) do + def merge_requests_count(user = nil) + mrs_count + end + end + + attr_reader :current_user, :issuable_collection + + def initialize(current_user, issuable_collection) + @current_user = current_user + @issuable_collection = issuable_collection + + validate_collection! + end + + def data + return {} if issuable_ids.empty? + + issuable_ids.each_with_object({}) do |id, issuable_meta| + issuable_meta[id] = metadata_for_issuable(id) + end + end + + private + + def metadata_for_issuable(id) + downvotes = group_issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } + upvotes = group_issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } + notes = grouped_issuable_notes_count.find { |notes| notes.noteable_id == id } + merge_requests = grouped_issuable_merge_requests_count.find { |mr| mr.first == id } + + IssuableMeta.new( + upvotes.try(:count).to_i, + downvotes.try(:count).to_i, + notes.try(:count).to_i, + merge_requests.try(:last).to_i + ) + end + + def validate_collection! # ActiveRecord uses Object#extend for null relations. if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) && issuable_collection.respond_to?(:limit_value) && @@ -10,36 +54,43 @@ module Gitlab raise 'Collection must have a limit applied for preloading meta-data' end + end - # map has to be used here since using pluck or select will - # throw an error when ordering issuables by priority which inserts - # a new order into the collection. - # We cannot use reorder to not mess up the paginated collection. - issuable_ids = issuable_collection.map(&:id) + def issuable_ids + strong_memoize(:issuable_ids) do + # map has to be used here since using pluck or select will + # throw an error when ordering issuables by priority which inserts + # a new order into the collection. + # We cannot use reorder to not mess up the paginated collection. + issuable_collection.map(&:id) + end + end - return {} if issuable_ids.empty? + def collection_type + # Supports relations or paginated arrays + issuable_collection.try(:model)&.name || + issuable_collection.first&.model_name.to_s + end - issuable_notes_count = ::Note.count_for_collection(issuable_ids, collection_type) - issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type) - issuable_merge_requests_count = + def group_issuable_votes_count + strong_memoize(:group_issuable_votes_count) do + AwardEmoji.votes_for_collection(issuable_ids, collection_type) + end + end + + def grouped_issuable_notes_count + strong_memoize(:grouped_issuable_notes_count) do + ::Note.count_for_collection(issuable_ids, collection_type) + end + end + + def grouped_issuable_merge_requests_count + strong_memoize(:grouped_issuable_merge_requests_count) do if collection_type == 'Issue' - ::MergeRequestsClosingIssues.count_for_collection(issuable_ids, user) + ::MergeRequestsClosingIssues.count_for_collection(issuable_ids, current_user) else [] end - - issuable_ids.each_with_object({}) do |id, issuable_meta| - downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } - upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } - notes = issuable_notes_count.find { |notes| notes.noteable_id == id } - merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id } - - issuable_meta[id] = ::Issuable::IssuableMeta.new( - upvotes.try(:count).to_i, - downvotes.try(:count).to_i, - notes.try(:count).to_i, - merge_requests.try(:last).to_i - ) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6fb263e7b0a..cdd98ff85a8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2231,9 +2231,6 @@ msgstr "" msgid "An error occurred while loading chart data" msgstr "" -msgid "An error occurred while loading clusters" -msgstr "" - msgid "An error occurred while loading commit signatures" msgstr "" @@ -5429,6 +5426,9 @@ msgstr "" msgid "ClusterIntergation|Select service role" msgstr "" +msgid "Clusters|An error occurred while loading clusters" +msgstr "" + msgid "Code" msgstr "" diff --git a/package.json b/package.json index 771584d5499..6dda80eb8da 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@babel/preset-env": "^7.8.4", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.130.0", - "@gitlab/ui": "16.0", + "@gitlab/ui": "16.0.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3", "@sentry/browser": "^5.10.2", diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index d4a12e0dc52..fc1328c887a 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -42,6 +42,13 @@ describe Admin::ClustersController do expect(response).to match_response_schema('cluster_list') end + it 'sets the polling interval header for json requests' do + get_index(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Poll-Interval']).to eq("10000") + end + context 'when page is specified' do let(:last_page) { Clusters::Cluster.instance_type.page.total_pages } let(:total_count) { Clusters::Cluster.instance_type.page.total_count } diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 1f2f6bd811b..57a6db54338 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -47,6 +47,13 @@ describe Groups::ClustersController do expect(response).to match_response_schema('cluster_list') end + it 'sets the polling interval header for json requests' do + go(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Poll-Interval']).to eq("10000") + end + context 'when page is specified' do let(:last_page) { group.clusters.page.total_pages } let(:total_count) { group.clusters.page.total_count } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 698a3773d59..262a4956ce5 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -41,6 +41,13 @@ describe Projects::ClustersController do expect(response).to match_response_schema('cluster_list') end + it 'sets the polling interval header for json requests' do + go(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Poll-Interval']).to eq("10000") + end + context 'when page is specified' do let(:last_page) { project.clusters.page.total_pages } let(:total_count) { project.clusters.page.total_count } diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 7dad5565856..831bcf8931e 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -95,45 +95,6 @@ describe 'issue move to another project' do expect(page).to have_no_selector('#move_to_project_id') end end - - context 'service desk issue moved to a project with service desk disabled', :js do - let(:project_title) { 'service desk disabled project' } - let(:warning_selector) { '.js-alert-moved-from-service-desk-warning' } - let(:namespace) { create(:namespace) } - let(:regular_project) { create(:project, title: project_title, service_desk_enabled: false) } - let(:service_desk_project) { build(:project, :private, namespace: namespace, service_desk_enabled: true) } - let(:service_desk_issue) { create(:issue, project: service_desk_project, author: ::User.support_bot) } - - before do - allow(::Gitlab).to receive(:com?).and_return(true) - allow(::Gitlab::IncomingEmail).to receive(:enabled?).and_return(true) - allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true) - - regular_project.add_reporter(user) - service_desk_project.add_reporter(user) - - visit issue_path(service_desk_issue) - - find('.js-move-issue').click - wait_for_requests - find('.js-move-issue-dropdown-item', text: project_title).click - find('.js-move-issue-confirmation-button').click - end - - it 'shows an alert after being moved' do - expect(page).to have_content('This project does not have Service Desk enabled') - end - - it 'does not show an alert after being dismissed' do - find("#{warning_selector} .js-close").click - - expect(page).to have_no_selector(warning_selector) - - page.refresh - - expect(page).to have_no_selector(warning_selector) - end - end end def issue_path(issue) diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js index e2d2e4b73b3..4ca2ccba385 100644 --- a/spec/frontend/clusters_list/components/clusters_spec.js +++ b/spec/frontend/clusters_list/components/clusters_spec.js @@ -28,13 +28,17 @@ describe('Clusters', () => { return axios.waitForAll(); }; + const paginationHeader = (total = apiData.clusters.length, perPage = 20, currentPage = 1) => { + return { + 'x-total': total, + 'x-per-page': perPage, + 'x-page': currentPage, + }; + }; + beforeEach(() => { mock = new MockAdapter(axios); - mockPollingApi(200, apiData, { - 'x-total': apiData.clusters.length, - 'x-per-page': 20, - 'x-page': 1, - }); + mockPollingApi(200, apiData, paginationHeader()); return mountWrapper(); }); @@ -99,17 +103,30 @@ describe('Clusters', () => { }); }); + describe('nodes present', () => { + it.each` + nodeSize | lineNumber + ${'Unknown'} | ${0} + ${'1'} | ${1} + ${'2'} | ${2} + ${'Unknown'} | ${3} + ${'Unknown'} | ${4} + ${'Unknown'} | ${5} + `('renders node size for each cluster', ({ nodeSize, lineNumber }) => { + const sizes = findTable().findAll('td:nth-child(3)'); + const size = sizes.at(lineNumber); + + expect(size.text()).toBe(nodeSize); + }); + }); + describe('pagination', () => { const perPage = apiData.clusters.length; const totalFirstPage = 100; const totalSecondPage = 500; beforeEach(() => { - mockPollingApi(200, apiData, { - 'x-total': totalFirstPage, - 'x-per-page': perPage, - 'x-page': 1, - }); + mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1)); return mountWrapper(); }); @@ -123,11 +140,7 @@ describe('Clusters', () => { describe('when updating currentPage', () => { beforeEach(() => { - mockPollingApi(200, apiData, { - 'x-total': totalSecondPage, - 'x-per-page': perPage, - 'x-page': 2, - }); + mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2)); wrapper.setData({ currentPage: 2 }); return axios.waitForAll(); }); diff --git a/spec/frontend/clusters_list/mock_data.js b/spec/frontend/clusters_list/mock_data.js index 9a90a378f31..893061f86e8 100644 --- a/spec/frontend/clusters_list/mock_data.js +++ b/spec/frontend/clusters_list/mock_data.js @@ -1,57 +1,45 @@ export const clusterList = [ { name: 'My Cluster 1', - environmentScope: '*', - size: '3', - clusterType: 'group_type', + environment_scope: '*', + cluster_type: 'group_type', status: 'disabled', - cpu: '6 (100% free)', - memory: '22.50 (30% free)', + nodes: null, }, { name: 'My Cluster 2', - environmentScope: 'development', - size: '12', - clusterType: 'project_type', + environment_scope: 'development', + cluster_type: 'project_type', status: 'unreachable', - cpu: '3 (50% free)', - memory: '11 (60% free)', + nodes: [{ usage: { cpu: '246155922n', memory: '1255212Ki' } }], }, { name: 'My Cluster 3', - environmentScope: 'development', - size: '12', - clusterType: 'project_type', + environment_scope: 'development', + cluster_type: 'project_type', status: 'authentication_failure', - cpu: '1 (0% free)', - memory: '22 (33% free)', + nodes: [ + { usage: { cpu: '246155922n', memory: '1255212Ki' } }, + { usage: { cpu: '307051934n', memory: '1379136Ki' } }, + ], }, { name: 'My Cluster 4', - environmentScope: 'production', - size: '12', - clusterType: 'project_type', + environment_scope: 'production', + cluster_type: 'project_type', status: 'deleting', - cpu: '6 (100% free)', - memory: '45 (15% free)', }, { name: 'My Cluster 5', - environmentScope: 'development', - size: '12', - clusterType: 'project_type', + environment_scope: 'development', + cluster_type: 'project_type', status: 'created', - cpu: '6 (100% free)', - memory: '20.12 (35% free)', }, { name: 'My Cluster 6', - environmentScope: '*', - size: '1', - clusterType: 'project_type', + environment_scope: '*', + cluster_type: 'project_type', status: 'cleanup_ongoing', - cpu: '6 (100% free)', - memory: '20.12 (35% free)', }, ]; diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js index 70766af3ec4..74e351a3704 100644 --- a/spec/frontend/clusters_list/store/actions_spec.js +++ b/spec/frontend/clusters_list/store/actions_spec.js @@ -1,10 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; +import Poll from '~/lib/utils/poll'; import flashError from '~/flash'; import testAction from 'helpers/vuex_action_helper'; import axios from '~/lib/utils/axios_utils'; +import waitForPromises from 'helpers/wait_for_promises'; import { apiData } from '../mock_data'; +import { MAX_REQUESTS } from '~/clusters_list/constants'; import * as types from '~/clusters_list/store/mutation_types'; import * as actions from '~/clusters_list/store/actions'; +import * as Sentry from '@sentry/browser'; jest.mock('~/flash.js'); @@ -12,6 +16,24 @@ describe('Clusters store actions', () => { describe('fetchClusters', () => { let mock; + const headers = { + 'x-next-page': 1, + 'x-total': apiData.clusters.length, + 'x-total-pages': 1, + 'x-per-page': 20, + 'x-page': 1, + 'x-prev-page': 1, + }; + + const paginationInformation = { + nextPage: 1, + page: 1, + perPage: 20, + previousPage: 1, + total: apiData.clusters.length, + totalPages: 1, + }; + beforeEach(() => { mock = new MockAdapter(axios); }); @@ -19,21 +41,6 @@ describe('Clusters store actions', () => { afterEach(() => mock.restore()); it('should commit SET_CLUSTERS_DATA with received response', done => { - const headers = { - 'x-total': apiData.clusters.length, - 'x-per-page': 20, - 'x-page': 1, - }; - - const paginationInformation = { - nextPage: NaN, - page: 1, - perPage: 20, - previousPage: NaN, - total: apiData.clusters.length, - totalPages: NaN, - }; - mock.onGet().reply(200, apiData, headers); testAction( @@ -52,9 +59,110 @@ describe('Clusters store actions', () => { it('should show flash on API error', done => { mock.onGet().reply(400, 'Not Found'); - testAction(actions.fetchClusters, { endpoint: apiData.endpoint }, {}, [], [], () => { - expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error')); - done(); + testAction( + actions.fetchClusters, + { endpoint: apiData.endpoint }, + {}, + [{ type: types.SET_LOADING_STATE, payload: false }], + [], + () => { + expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error')); + done(); + }, + ); + }); + + describe('multiple api requests', () => { + let captureException; + let pollRequest; + let pollStop; + + const pollInterval = 10; + const pollHeaders = { 'poll-interval': pollInterval, ...headers }; + + beforeEach(() => { + captureException = jest.spyOn(Sentry, 'captureException'); + pollRequest = jest.spyOn(Poll.prototype, 'makeRequest'); + pollStop = jest.spyOn(Poll.prototype, 'stop'); + + mock.onGet().reply(200, apiData, pollHeaders); + }); + + afterEach(() => { + captureException.mockRestore(); + pollRequest.mockRestore(); + pollStop.mockRestore(); + }); + + it('should stop polling after MAX Requests', done => { + testAction( + actions.fetchClusters, + { endpoint: apiData.endpoint }, + {}, + [ + { type: types.SET_CLUSTERS_DATA, payload: { data: apiData, paginationInformation } }, + { type: types.SET_LOADING_STATE, payload: false }, + ], + [], + () => { + expect(pollRequest).toHaveBeenCalledTimes(1); + expect(pollStop).toHaveBeenCalledTimes(0); + jest.advanceTimersByTime(pollInterval); + + waitForPromises() + .then(() => { + expect(pollRequest).toHaveBeenCalledTimes(2); + expect(pollStop).toHaveBeenCalledTimes(0); + jest.advanceTimersByTime(pollInterval); + }) + .then(() => waitForPromises()) + .then(() => { + expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS); + expect(pollStop).toHaveBeenCalledTimes(0); + jest.advanceTimersByTime(pollInterval); + }) + .then(() => waitForPromises()) + .then(() => { + expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS + 1); + // Stops poll once it exceeds the MAX_REQUESTS limit + expect(pollStop).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(pollInterval); + }) + .then(() => waitForPromises()) + .then(() => { + // Additional poll requests are not made once pollStop is called + expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS + 1); + expect(pollStop).toHaveBeenCalledTimes(1); + }) + .then(done) + .catch(done.fail); + }, + ); + }); + + it('should stop polling and report to Sentry when data is invalid', done => { + const badApiResponse = { clusters: {} }; + mock.onGet().reply(200, badApiResponse, pollHeaders); + + testAction( + actions.fetchClusters, + { endpoint: apiData.endpoint }, + {}, + [ + { + type: types.SET_CLUSTERS_DATA, + payload: { data: badApiResponse, paginationInformation }, + }, + { type: types.SET_LOADING_STATE, payload: false }, + ], + [], + () => { + expect(pollRequest).toHaveBeenCalledTimes(1); + expect(pollStop).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledTimes(1); + done(); + }, + ); }); }); }); diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb index 7632bc3060a..1920cecfc29 100644 --- a/spec/lib/gitlab/issuable_metadata_spec.rb +++ b/spec/lib/gitlab/issuable_metadata_spec.rb @@ -6,14 +6,12 @@ describe Gitlab::IssuableMetadata do let(:user) { create(:user) } let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } - subject { Class.new { include Gitlab::IssuableMetadata }.new } - it 'returns an empty Hash if an empty collection is provided' do - expect(subject.issuable_meta_data(Issue.none, 'Issue', user)).to eq({}) + expect(described_class.new(user, Issue.none).data).to eq({}) end it 'raises an error when given a collection with no limit' do - expect { subject.issuable_meta_data(Issue.all, 'Issue', user) }.to raise_error(/must have a limit/) + expect { described_class.new(user, Issue.all) }.to raise_error(/must have a limit/) end context 'issues' do @@ -25,7 +23,7 @@ describe Gitlab::IssuableMetadata do let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) } it 'aggregates stats on issues' do - data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue', user) + data = described_class.new(user, Issue.all.limit(10)).data expect(data.count).to eq(2) expect(data[issue.id].upvotes).to eq(1) @@ -48,7 +46,7 @@ describe Gitlab::IssuableMetadata do let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } it 'aggregates stats on merge requests' do - data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest', user) + data = described_class.new(user, MergeRequest.all.limit(10)).data expect(data.count).to eq(2) expect(data[merge_request.id].upvotes).to eq(1) diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index f741d2d9acf..296240b1602 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -263,7 +263,7 @@ describe Ci::Runner do subject { described_class.online } before do - @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago) + @runner1 = create(:ci_runner, :instance, contacted_at: 2.hours.ago) @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago) end @@ -344,7 +344,7 @@ describe Ci::Runner do subject { described_class.offline } before do - @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago) + @runner1 = create(:ci_runner, :instance, contacted_at: 2.hours.ago) @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago) end @@ -598,10 +598,10 @@ describe Ci::Runner do end end - describe '#update_cached_info' do + describe '#heartbeat' do let(:runner) { create(:ci_runner, :project) } - subject { runner.update_cached_info(architecture: '18-bit') } + subject { runner.heartbeat(architecture: '18-bit') } context 'when database was updated recently' do before do diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb index 62a5d9a7cf9..ae14adf9106 100644 --- a/spec/models/iteration_spec.rb +++ b/spec/models/iteration_spec.rb @@ -46,10 +46,7 @@ describe Iteration do end context 'when dates overlap' do - let(:start_date) { 5.days.from_now } - let(:due_date) { 6.days.from_now } - - shared_examples_for 'overlapping dates' do + context 'same group' do context 'when start_date is in range' do let(:start_date) { 5.days.from_now } let(:due_date) { 3.weeks.from_now } @@ -58,11 +55,6 @@ describe Iteration do expect(subject).not_to be_valid expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') end - - it 'is not valid even if forced' do - subject.validate # to generate iid/etc - expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/) - end end context 'when end_date is in range' do @@ -73,84 +65,25 @@ describe Iteration do expect(subject).not_to be_valid expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') end - - it 'is not valid even if forced' do - subject.validate # to generate iid/etc - expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/) - end end context 'when both overlap' do + let(:start_date) { 5.days.from_now } + let(:due_date) { 6.days.from_now } + it 'is not valid' do expect(subject).not_to be_valid expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') end - - it 'is not valid even if forced' do - subject.validate # to generate iid/etc - expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/) - end end end - context 'group' do - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' } - end - - context 'different group' do - let(:group) { create(:group) } - - it { is_expected.to be_valid } - - it 'does not trigger exclusion constraints' do - expect { subject.save }.not_to raise_exception - end - end - - context 'in a project' do - let(:project) { create(:project) } - - subject { build(:iteration, project: project, start_date: start_date, due_date: due_date) } - - it { is_expected.to be_valid } + context 'different group' do + let(:start_date) { 5.days.from_now } + let(:due_date) { 6.days.from_now } + let(:group) { create(:group) } - it 'does not trigger exclusion constraints' do - expect { subject.save }.not_to raise_exception - end - end - end - - context 'project' do - let_it_be(:existing_iteration) { create(:iteration, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - - subject { build(:iteration, project: project, start_date: start_date, due_date: due_date) } - - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } - end - - context 'different project' do - let(:project) { create(:project) } - - it { is_expected.to be_valid } - - it 'does not trigger exclusion constraints' do - expect { subject.save }.not_to raise_exception - end - end - - context 'in a group' do - let(:group) { create(:group) } - - subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) } - - it { is_expected.to be_valid } - - it 'does not trigger exclusion constraints' do - expect { subject.save }.not_to raise_exception - end - end + it { is_expected.to be_valid } end end end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 913dcb63d6e..6d4495a255d 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1129,6 +1129,10 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let(:send_request) { update_job(state: 'success') } end + it 'updates runner info' do + expect { update_job(state: 'success') }.to change { runner.reload.contacted_at } + end + context 'when status is given' do it 'mark job as succeeded' do update_job(state: 'success') @@ -1294,6 +1298,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let(:send_request) { patch_the_trace } end + it 'updates runner info' do + runner.update!(contacted_at: 1.year.ago) + + expect { patch_the_trace }.to change { runner.reload.contacted_at } + end + context 'when request is valid' do it 'gets correct response' do expect(response).to have_gitlab_http_status(:accepted) @@ -1555,6 +1565,10 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let(:send_request) { subject } end + it 'updates runner info' do + expect { subject }.to change { runner.reload.contacted_at } + end + shared_examples 'authorizes local file' do it 'succeeds' do subject @@ -1743,6 +1757,10 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + it 'updates runner info' do + expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at } + end + context 'when artifacts are being stored inside of tmp path' do before do # by configuring this path we allow to pass temp file from any path @@ -2228,6 +2246,10 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let(:send_request) { download_artifact } end + it 'updates runner info' do + expect { download_artifact }.to change { runner.reload.contacted_at } + end + context 'when job has artifacts' do let(:job) { create(:ci_build) } let(:store) { JobArtifactUploader::Store::LOCAL } diff --git a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb index 2dbaea57c44..62a1a07b6c1 100644 --- a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb @@ -34,7 +34,7 @@ RSpec.shared_examples 'issuables list meta-data' do |issuable_type, action = nil aggregate_failures do expect(meta_data.keys).to match_array(issuables.map(&:id)) - expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta)) + expect(meta_data.values).to all(be_kind_of(Gitlab::IssuableMetadata::IssuableMeta)) end end diff --git a/yarn.lock b/yarn.lock index 0c2d52e6df2..b3f4be1897e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -787,7 +787,7 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.130.0.tgz#0c2f3cdc0a4b0f54c47b2861c8fa31b2a58c570a" integrity sha512-azJ1E9PBk6fGOaP6816BSr8oYrQu3m3BbYZwWOCUp8AfbZuf0ZOZVYmlR9i/eAOhoqqqmwF8hYCK2VjAklbpPA== -"@gitlab/ui@16.0": +"@gitlab/ui@16.0.0": version "16.0.0" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-16.0.0.tgz#0e2d19b85c47f45a052caf6cd0367613cbab8e8e" integrity sha512-xSWXtFWWQzGtL35dGexc5LGqAJXYjLMEFQyPLzCBX3yY9tkI9s9rVMX053tnKYb9kgEmL+R/xGiW7D9nb58VmQ== |