diff options
84 files changed, 587 insertions, 2521 deletions
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 5ab66acaf80..2e187eae17c 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,56 +1,57 @@ -/* eslint-disable no-param-reassign, consistent-return */ - +import { parseBoolean } from '~/lib/utils/common_utils'; import AccessorUtilities from './lib/utils/accessor'; export default class Autosave { constructor(field, key, fallbackKey, lockVersion) { this.field = field; - this.type = this.field.prop('type'); + this.type = this.field.getAttribute('type'); this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage(); - if (key.join != null) { - key = key.join('/'); - } - this.key = `autosave/${key}`; + this.key = Array.isArray(key) ? `autosave/${key.join('/')}` : `autosave/${key}`; this.fallbackKey = fallbackKey; this.lockVersionKey = `${this.key}/lockVersion`; this.lockVersion = lockVersion; - this.field.data('autosave', this); this.restore(); - this.field.on('input', () => this.save()); + this.saveAction = this.save.bind(this); + // used by app/assets/javascripts/deprecated_notes.js + this.field.$autosave = this; + this.field.addEventListener('input', this.saveAction); } restore() { if (!this.isLocalStorageAvailable) return; - if (!this.field.length) return; const text = window.localStorage.getItem(this.key); const fallbackText = window.localStorage.getItem(this.fallbackKey); + const newValue = text || fallbackText; + if (newValue == null) return; + + let originalValue = this.field.value; if (this.type === 'checkbox') { - this.field.prop('checked', text || fallbackText); - } else if (text) { - this.field.val(text); - } else if (fallbackText) { - this.field.val(fallbackText); + originalValue = this.field.checked; + this.field.checked = parseBoolean(newValue); + } else { + this.field.value = newValue; } - this.field.trigger('input'); - // v-model does not update with jQuery trigger - // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137 - const event = new Event('change', { bubbles: true, cancelable: false }); - const field = this.field.get(0); - if (field) { - field.dispatchEvent(event); - } + if (originalValue === newValue) return; + this.triggerInputEvents(); + } + + triggerInputEvents() { + // trigger events so @input, @change and v-model trigger in Vue components + const inputEvent = new Event('input', { bubbles: true, cancelable: false }); + const changeEvent = new Event('change', { bubbles: true, cancelable: false }); + this.field.dispatchEvent(inputEvent); + this.field.dispatchEvent(changeEvent); } getSavedLockVersion() { - if (!this.isLocalStorageAvailable) return; + if (!this.isLocalStorageAvailable) return undefined; return window.localStorage.getItem(this.lockVersionKey); } save() { - if (!this.field.length) return; - const value = this.type === 'checkbox' ? this.field.is(':checked') : this.field.val(); + const value = this.type === 'checkbox' ? this.field.checked : this.field.value; if (this.isLocalStorageAvailable && value) { if (this.fallbackKey) { @@ -66,7 +67,7 @@ export default class Autosave { } reset() { - if (!this.isLocalStorageAvailable) return; + if (!this.isLocalStorageAvailable) return undefined; window.localStorage.removeItem(this.lockVersionKey); window.localStorage.removeItem(this.fallbackKey); @@ -74,7 +75,7 @@ export default class Autosave { } dispose() { - // eslint-disable-next-line @gitlab/no-global-event-off - this.field.off('input'); + delete this.field.$autosave; + this.field.removeEventListener('input', this.saveAction); } } diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index acc3cbe10a0..ed0481e7a48 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -1,5 +1,4 @@ <script> -import $ from 'jquery'; import { GlDropdown, GlButton, @@ -52,7 +51,7 @@ export default { }, mounted() { this.autosave = new Autosave( - $(this.$refs.textarea), + this.$refs.textarea, `submit_review_dropdown/${this.getNoteableData.id}`, ); this.noteData.noteable_type = this.noteableType; diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js index 5c6874593a4..8019a10a042 100644 --- a/app/assets/javascripts/deprecated_notes.js +++ b/app/assets/javascripts/deprecated_notes.js @@ -575,7 +575,9 @@ export default class Notes { // reset text and preview form.find('.js-md-write-button').click(); form.find('.js-note-text').val('').trigger('input'); - form.find('.js-note-text').data('autosave').reset(); + form.find('.js-note-text').each(function reset() { + this.$autosave.reset(); + }); const event = document.createEvent('Event'); event.initEvent('autosize:update', true, false); @@ -642,7 +644,9 @@ export default class Notes { // DiffNote form.find('#note_position').val(), ]; - return new Autosave(textarea, key); + const textareaEl = textarea.get(0); + // eslint-disable-next-line no-new + if (textareaEl) new Autosave(textareaEl, key); } /** @@ -1086,7 +1090,9 @@ export default class Notes { const row = form.closest('tr'); const glForm = form.data('glForm'); glForm.destroy(); - form.find('.js-note-text').data('autosave').reset(); + form.find('.js-note-text').each(function reset() { + this.$autosave.reset(); + }); // show the reply button (will only work for replies) form.prev('.discussion-reply-holder').show(); if (row.is('.js-temp-notes-holder')) { diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 5a6b220e532..830f16b50ee 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -1,6 +1,5 @@ <script> import { GlButton } from '@gitlab/ui'; -import $ from 'jquery'; import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; import Autosave from '~/autosave'; @@ -118,7 +117,7 @@ export default { }, initAutosaveComment() { if (this.isLoggedIn) { - this.autosaveDiscussion = new Autosave($(this.$refs.textarea), [ + this.autosaveDiscussion = new Autosave(this.$refs.textarea, [ s__('DesignManagement|Discussion'), getIdFromGraphQLId(this.noteableId), this.shortDiscussionId, diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js index e8ba99e0e9e..99a3f76ca76 100644 --- a/app/assets/javascripts/issuable/issuable_form.js +++ b/app/assets/javascripts/issuable/issuable_form.js @@ -47,13 +47,12 @@ function getFallbackKey() { } export default class IssuableForm { - static addAutosave(map, id, $input, searchTerm, fallbackKey) { - if ($input.length) { - map.set( - id, - new Autosave($input, [document.location.pathname, searchTerm, id], `${fallbackKey}=${id}`), - ); - } + static addAutosave(map, id, element, searchTerm, fallbackKey) { + if (!element) return; + map.set( + id, + new Autosave(element, [document.location.pathname, searchTerm, id], `${fallbackKey}=${id}`), + ); } constructor(form) { @@ -122,28 +121,28 @@ export default class IssuableForm { IssuableForm.addAutosave( autosaveMap, 'title', - this.form.find('input[name*="[title]"]'), + this.form.find('input[name*="[title]"]').get(0), this.searchTerm, this.fallbackKey, ); IssuableForm.addAutosave( autosaveMap, 'description', - this.form.find('textarea[name*="[description]"]'), + this.form.find('textarea[name*="[description]"]').get(0), this.searchTerm, this.fallbackKey, ); IssuableForm.addAutosave( autosaveMap, 'confidential', - this.form.find('input:checkbox[name*="[confidential]"]'), + this.form.find('input:checkbox[name*="[confidential]"]').get(0), this.searchTerm, this.fallbackKey, ); IssuableForm.addAutosave( autosaveMap, 'due_date', - this.form.find('input[name*="[due_date]"]'), + this.form.find('input[name*="[due_date]"]').get(0), this.searchTerm, this.fallbackKey, ); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 2ccb9a0b514..c6e7117cf2e 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -312,7 +312,7 @@ export default { if (this.isLoggedIn) { const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); - this.autosave = new Autosave($(this.$refs.textarea), [ + this.autosave = new Autosave(this.$refs.textarea, [ this.$options.i18n.note, noteableType, this.getNoteableData.id, diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 61cb4ab2a10..17272d5abef 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import { s__ } from '~/locale'; import Autosave from '~/autosave'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; @@ -16,7 +15,7 @@ export default { keys = keys.concat(extraKeys); } - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys); + this.autosave = new Autosave(this.$refs.noteForm.$refs.textarea, keys); }, resetAutoSave() { this.autosave.reset(); diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue index b1809e6a9f3..a7d3bcfd59f 100644 --- a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue +++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue @@ -1,25 +1,32 @@ <script> -import { GlListbox } from '@gitlab/ui'; +import { GlFormGroup, GlListbox } from '@gitlab/ui'; import { __ } from '~/locale'; -const MIN_ITEMS_COUNT_FOR_SEARCHING = 20; +const MIN_ITEMS_COUNT_FOR_SEARCHING = 10; export default { i18n: { noResultsText: __('No results found'), }, components: { + GlFormGroup, GlListbox, }, model: GlListbox.model, props: { + label: { + type: String, + required: false, + default: '', + }, name: { type: String, required: true, }, defaultToggleText: { type: String, - required: true, + required: false, + default: '', }, selected: { type: String, @@ -95,7 +102,7 @@ export default { </script> <template> - <div> + <gl-form-group :label="label"> <gl-listbox :selected="selected" :toggle-text="toggleText" @@ -106,5 +113,5 @@ export default { @select="$emit($options.model.event, $event)" /> <input ref="input" type="hidden" :name="name" :value="selected" /> - </div> + </gl-form-group> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index a28460dd58e..f382ded90d7 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -140,3 +140,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip'; export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control'; export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight'; + +// We fallback to highlighting these languages with Rouge, see the following issue for more detail: +// https://gitlab.com/gitlab-org/gitlab/-/issues/384375#note_1212752013 +export const LEGACY_FALLBACKS = ['python']; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 0cfee93ce5d..efafa67a733 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -11,6 +11,7 @@ import { EVENT_LABEL_FALLBACK, ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK, + LEGACY_FALLBACKS, } from './constants'; import Chunk from './components/chunk.vue'; import { registerPlugins } from './plugins/index'; @@ -57,10 +58,11 @@ export default { }, unsupportedLanguage() { const supportedLanguages = Object.keys(languageLoader); - return ( + const unsupportedLanguage = !supportedLanguages.includes(this.language) && - !supportedLanguages.includes(this.blob.language?.toLowerCase()) - ); + !supportedLanguages.includes(this.blob.language?.toLowerCase()); + + return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage; }, totalChunks() { return Object.keys(this.chunks).length; diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue index 2fc1f935501..387fc5e0d1c 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue @@ -1,6 +1,5 @@ <script> import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; -import $ from 'jquery'; import Autosave from '~/autosave'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; @@ -81,13 +80,13 @@ export default { if (!titleInput || !descriptionInput) return; - this.autosaveTitle = new Autosave($(titleInput.$el), [ + this.autosaveTitle = new Autosave(titleInput.$el, [ document.location.pathname, document.location.search, 'title', ]); - this.autosaveDescription = new Autosave($(descriptionInput.$el), [ + this.autosaveDescription = new Autosave(descriptionInput, [ document.location.pathname, document.location.search, 'description', diff --git a/app/controllers/concerns/check_rate_limit.rb b/app/controllers/concerns/check_rate_limit.rb index 0eaf74fd3a9..fc3be3ad009 100644 --- a/app/controllers/concerns/check_rate_limit.rb +++ b/app/controllers/concerns/check_rate_limit.rb @@ -8,10 +8,7 @@ # See lib/api/helpers/rate_limiter.rb for API version module CheckRateLimit def check_rate_limit!(key, scope:, redirect_back: false, **options) - return if bypass_header_set? - return unless rate_limiter.throttled?(key, scope: scope, **options) - - rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) + return unless Gitlab::ApplicationRateLimiter.throttled_request?(request, current_user, key, scope: scope, **options) return yield if block_given? @@ -23,14 +20,4 @@ module CheckRateLimit render plain: message, status: :too_many_requests end end - - private - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end - - def bypass_header_set? - ::Gitlab::Throttle.bypass_header.present? && request.get_header(Gitlab::Throttle.bypass_header) == '1' - end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 7b0d8cf8dcb..5060ce69d9c 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -3,6 +3,7 @@ module IssuableCollections extend ActiveSupport::Concern include PaginatedCollection + include SearchRateLimitable include SortingHelper include SortingPreference include Gitlab::Utils::StrongMemoize diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb index 7beb86b51fd..b8249345a54 100644 --- a/app/controllers/concerns/issuable_collections_action.rb +++ b/app/controllers/concerns/issuable_collections_action.rb @@ -5,6 +5,12 @@ module IssuableCollectionsAction include IssuableCollections include IssuesCalendar + included do + before_action :check_search_rate_limit!, only: [:issues, :merge_requests], if: -> { + params[:search].present? && Feature.enabled?(:rate_limit_issuable_searches) + } + end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def issues show_alert_if_search_is_disabled diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7dc7a4e55a8..7441ec46c28 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -27,6 +27,10 @@ class Projects::IssuesController < Projects::ApplicationController before_action :set_issuables_index, if: ->(c) { SET_ISSUABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) && !index_html_request? } + before_action :check_search_rate_limit!, if: ->(c) { + SET_ISSUABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) && !index_html_request? && + params[:search].present? && Feature.enabled?(:rate_limit_issuable_searches) + } # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3ab1f7d1d32..1b5ae7af252 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -28,6 +28,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo :codequality_mr_diff_reports ] before_action :set_issuables_index, only: [:index] + before_action :check_search_rate_limit!, only: [:index], if: -> { + params[:search].present? && Feature.enabled?(:rate_limit_issuable_searches) + } before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] diff --git a/app/graphql/resolvers/concerns/search_arguments.rb b/app/graphql/resolvers/concerns/search_arguments.rb index ccc012f2bf9..cc1a13fdf29 100644 --- a/app/graphql/resolvers/concerns/search_arguments.rb +++ b/app/graphql/resolvers/concerns/search_arguments.rb @@ -18,6 +18,7 @@ module SearchArguments def ready?(**args) validate_search_in_params!(args) validate_anonymous_search_access!(args) + validate_search_rate_limit!(args) super end @@ -39,6 +40,28 @@ module SearchArguments '`search` should be present when including the `in` argument' end + def validate_search_rate_limit!(args) + return if args[:search].blank? || context[:request].nil? || Feature.disabled?(:rate_limit_issuable_searches) + + if current_user.present? + rate_limiter_key = :search_rate_limit + rate_limiter_scope = [current_user] + else + rate_limiter_key = :search_rate_limit_unauthenticated + rate_limiter_scope = [context[:request].ip] + end + + if ::Gitlab::ApplicationRateLimiter.throttled_request?( + context[:request], + current_user, + rate_limiter_key, + scope: rate_limiter_scope + ) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'This endpoint has been requested with the search argument too many times. Try again later.' + end + end + def prepare_finder_params(args) prepare_search_params(args) end diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb index f0f56d9ebd9..969820459e3 100644 --- a/app/models/clusters/providers/aws.rb +++ b/app/models/clusters/providers/aws.rb @@ -45,18 +45,6 @@ module Clusters ) end - def api_client - strong_memoize(:api_client) do - ::Aws::CloudFormation::Client.new(credentials: credentials, region: region) - end - end - - def credentials - strong_memoize(:credentials) do - ::Aws::Credentials.new(access_key_id, secret_access_key, session_token) - end - end - def has_rbac_enabled? true end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index fde5ed592cb..6f39037b947 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -37,12 +37,6 @@ module Clusters greater_than: 0 } - def api_client - return unless access_token - - @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil) - end - def nullify_credentials assign_attributes( access_token: nil, diff --git a/app/services/clusters/aws/authorize_role_service.rb b/app/services/clusters/aws/authorize_role_service.rb deleted file mode 100644 index 7ca20289bf7..00000000000 --- a/app/services/clusters/aws/authorize_role_service.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class AuthorizeRoleService - attr_reader :user - - Response = Struct.new(:status, :body) - - ERRORS = [ - ActiveRecord::RecordInvalid, - ActiveRecord::RecordNotFound, - Clusters::Aws::FetchCredentialsService::MissingRoleError, - ::Aws::Errors::MissingCredentialsError, - ::Aws::STS::Errors::ServiceError - ].freeze - - def initialize(user, params:) - @user = user - @role_arn = params[:role_arn] - @region = params[:region] - end - - def execute - ensure_role_exists! - update_role_arn! - - Response.new(:ok, credentials) - rescue *ERRORS => e - Gitlab::ErrorTracking.track_exception(e) - - Response.new(:unprocessable_entity, response_details(e)) - end - - private - - attr_reader :role, :role_arn, :region - - def ensure_role_exists! - @role = ::Aws::Role.find_by_user_id!(user.id) - end - - def update_role_arn! - role.update!(role_arn: role_arn, region: region) - end - - def credentials - Clusters::Aws::FetchCredentialsService.new(role).execute - end - - def response_details(exception) - message = - case exception - when ::Aws::STS::Errors::AccessDenied - _("Access denied: %{error}") % { error: exception.message } - when ::Aws::STS::Errors::ServiceError - _("AWS service error: %{error}") % { error: exception.message } - when ActiveRecord::RecordNotFound - _("Error: Unable to find AWS role for current user") - when ActiveRecord::RecordInvalid - exception.message - when Clusters::Aws::FetchCredentialsService::MissingRoleError - _("Error: No AWS provision role found for user") - when ::Aws::Errors::MissingCredentialsError - _("Error: No AWS credentials were supplied") - else - _('An error occurred while authorizing your role') - end - - { message: message }.compact - end - end - end -end diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb deleted file mode 100644 index e38852c7ec7..00000000000 --- a/app/services/clusters/aws/fetch_credentials_service.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class FetchCredentialsService - attr_reader :provision_role - - MissingRoleError = Class.new(StandardError) - - def initialize(provision_role, provider: nil) - @provision_role = provision_role - @provider = provider - @region = provider&.region || provision_role&.region || Clusters::Providers::Aws::DEFAULT_REGION - end - - def execute - raise MissingRoleError, 'AWS provisioning role not configured' unless provision_role.present? - - ::Aws::AssumeRoleCredentials.new( - client: client, - role_arn: provision_role.role_arn, - role_session_name: session_name, - external_id: provision_role.role_external_id, - policy: session_policy - ).credentials - end - - private - - attr_reader :provider, :region - - def client - ::Aws::STS::Client.new(**client_args) - end - - def client_args - { region: region, credentials: gitlab_credentials }.compact - end - - def gitlab_credentials - # These are not needed for IAM instance profiles - return unless access_key_id.present? && secret_access_key.present? - - ::Aws::Credentials.new(access_key_id, secret_access_key) - end - - def access_key_id - Gitlab::CurrentSettings.eks_access_key_id - end - - def secret_access_key - Gitlab::CurrentSettings.eks_secret_access_key - end - - ## - # If we haven't created a provider record yet, - # we restrict ourselves to read-only access so - # that we can safely expose credentials to the - # frontend (to be used when populating the - # creation form). - def session_policy - if provider.nil? - File.read(read_only_policy) - end - end - - def read_only_policy - Rails.root.join('vendor', 'aws', 'iam', "eks_cluster_read_only_policy.json") - end - - def session_name - if provider.present? - "gitlab-eks-cluster-#{provider.cluster_id}-user-#{provision_role.user_id}" - else - "gitlab-eks-autofill-user-#{provision_role.user_id}" - end - end - end - end -end diff --git a/app/services/clusters/aws/finalize_creation_service.rb b/app/services/clusters/aws/finalize_creation_service.rb deleted file mode 100644 index 54f07e1d44c..00000000000 --- a/app/services/clusters/aws/finalize_creation_service.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class FinalizeCreationService - include Gitlab::Utils::StrongMemoize - - attr_reader :provider - - delegate :cluster, to: :provider - - def execute(provider) - @provider = provider - - configure_provider - create_gitlab_service_account! - configure_platform_kubernetes - configure_node_authentication! - - cluster.save! - rescue ::Aws::CloudFormation::Errors::ServiceError => e - log_service_error(e.class.name, provider.id, e.message) - provider.make_errored!(s_('ClusterIntegration|Failed to fetch CloudFormation stack: %{message}') % { message: e.message }) - rescue Kubeclient::HttpError => e - log_service_error(e.class.name, provider.id, e.message) - provider.make_errored!(s_('ClusterIntegration|Failed to run Kubeclient: %{message}') % { message: e.message }) - rescue ActiveRecord::RecordInvalid => e - log_service_error(e.class.name, provider.id, e.message) - provider.make_errored!(s_('ClusterIntegration|Failed to configure EKS provider: %{message}') % { message: e.message }) - end - - private - - def create_gitlab_service_account! - Clusters::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator( - kube_client, - rbac: true - ).execute - end - - def configure_provider - provider.status_event = :make_created - end - - def configure_platform_kubernetes - cluster.build_platform_kubernetes( - api_url: cluster_endpoint, - ca_cert: cluster_certificate, - token: request_kubernetes_token) - end - - def request_kubernetes_token - Clusters::Kubernetes::FetchKubernetesTokenService.new( - kube_client, - Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, - Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE - ).execute - end - - def kube_client - @kube_client ||= build_kube_client!( - cluster_endpoint, - cluster_certificate - ) - end - - def build_kube_client!(api_url, ca_pem) - raise "Incomplete settings" unless api_url - - Gitlab::Kubernetes::KubeClient.new( - api_url, - auth_options: kubeclient_auth_options, - ssl_options: kubeclient_ssl_options(ca_pem), - http_proxy_uri: ENV['http_proxy'] - ) - end - - def kubeclient_auth_options - { bearer_token: Kubeclient::AmazonEksCredentials.token(provider.credentials, cluster.name) } - end - - def kubeclient_ssl_options(ca_pem) - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - - def cluster_stack - @cluster_stack ||= provider.api_client.describe_stacks(stack_name: provider.cluster.name).stacks.first - end - - def stack_output_value(key) - cluster_stack.outputs.detect { |output| output.output_key == key }.output_value - end - - def node_instance_role_arn - stack_output_value('NodeInstanceRole') - end - - def cluster_endpoint - strong_memoize(:cluster_endpoint) do - stack_output_value('ClusterEndpoint') - end - end - - def cluster_certificate - strong_memoize(:cluster_certificate) do - Base64.decode64(stack_output_value('ClusterCertificate')) - end - end - - def configure_node_authentication! - kube_client.create_config_map(node_authentication_config) - end - - def node_authentication_config - Gitlab::Kubernetes::ConfigMaps::AwsNodeAuth.new(node_instance_role_arn).generate - end - - def logger - @logger ||= Gitlab::Kubernetes::Logger.build - end - - def log_service_error(exception, provider_id, message) - logger.error( - exception: exception.class.name, - service: self.class.name, - provider_id: provider_id, - message: message - ) - end - end - end -end diff --git a/app/services/clusters/aws/provision_service.rb b/app/services/clusters/aws/provision_service.rb deleted file mode 100644 index b454a7a5f59..00000000000 --- a/app/services/clusters/aws/provision_service.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class ProvisionService - attr_reader :provider - - def execute(provider) - @provider = provider - - configure_provider_credentials - provision_cluster - - if provider.make_creating - WaitForClusterCreationWorker.perform_in( - Clusters::Aws::VerifyProvisionStatusService::INITIAL_INTERVAL, - provider.cluster_id - ) - else - provider.make_errored!("Failed to update provider record; #{provider.errors.full_messages}") - end - rescue Clusters::Aws::FetchCredentialsService::MissingRoleError - provider.make_errored!('Amazon role is not configured') - rescue ::Aws::Errors::MissingCredentialsError - provider.make_errored!('Amazon credentials are not configured') - rescue ::Aws::STS::Errors::ServiceError => e - provider.make_errored!("Amazon authentication failed; #{e.message}") - rescue ::Aws::CloudFormation::Errors::ServiceError => e - provider.make_errored!("Amazon CloudFormation request failed; #{e.message}") - end - - private - - def provision_role - provider.created_by_user&.aws_role - end - - def credentials - @credentials ||= Clusters::Aws::FetchCredentialsService.new( - provision_role, - provider: provider - ).execute - end - - def configure_provider_credentials - provider.update!( - access_key_id: credentials.access_key_id, - secret_access_key: credentials.secret_access_key, - session_token: credentials.session_token - ) - end - - def provision_cluster - provider.api_client.create_stack( - stack_name: provider.cluster.name, - template_body: stack_template, - parameters: parameters, - capabilities: ["CAPABILITY_IAM"] - ) - end - - def parameters - [ - parameter('ClusterName', provider.cluster.name), - parameter('ClusterRole', provider.role_arn), - parameter('KubernetesVersion', provider.kubernetes_version), - parameter('ClusterControlPlaneSecurityGroup', provider.security_group_id), - parameter('VpcId', provider.vpc_id), - parameter('Subnets', provider.subnet_ids.join(',')), - parameter('NodeAutoScalingGroupDesiredCapacity', provider.num_nodes.to_s), - parameter('NodeInstanceType', provider.instance_type), - parameter('KeyName', provider.key_name) - ] - end - - def parameter(key, value) - { parameter_key: key, parameter_value: value } - end - - def stack_template - File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml')) - end - end - end -end diff --git a/app/services/clusters/aws/verify_provision_status_service.rb b/app/services/clusters/aws/verify_provision_status_service.rb deleted file mode 100644 index 99532662bc4..00000000000 --- a/app/services/clusters/aws/verify_provision_status_service.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class VerifyProvisionStatusService - attr_reader :provider - - INITIAL_INTERVAL = 5.minutes - POLL_INTERVAL = 1.minute - TIMEOUT = 30.minutes - - def execute(provider) - @provider = provider - - case cluster_stack.stack_status - when 'CREATE_IN_PROGRESS' - continue_creation - when 'CREATE_COMPLETE' - finalize_creation - else - provider.make_errored!("Unexpected status; #{cluster_stack.stack_status}") - end - rescue ::Aws::CloudFormation::Errors::ServiceError => e - provider.make_errored!("Amazon CloudFormation request failed; #{e.message}") - end - - private - - def cluster_stack - @cluster_stack ||= provider.api_client.describe_stacks(stack_name: provider.cluster.name).stacks.first - end - - def continue_creation - if timeout_threshold.future? - WaitForClusterCreationWorker.perform_in(POLL_INTERVAL, provider.cluster_id) - else - provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT }) - end - end - - def timeout_threshold - cluster_stack.creation_time + TIMEOUT - end - - def finalize_creation - Clusters::Aws::FinalizeCreationService.new.execute(provider) - end - end - end -end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index cb2de8b943c..4c7384806ad 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -24,9 +24,7 @@ module Clusters return cluster if cluster.errors.present? - cluster.tap do |cluster| - cluster.save && ClusterProvisionWorker.perform_async(cluster.id) - end + cluster.tap(&:save) end private diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb deleted file mode 100644 index 6c648b443a0..00000000000 --- a/app/services/clusters/gcp/fetch_operation_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - class FetchOperationService - def execute(provider) - operation = provider.api_client.projects_zones_operations( - provider.gcp_project_id, - provider.zone, - provider.operation_id) - - yield(operation) if block_given? - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - logger.error( - exception: e.class.name, - service: self.class.name, - provider_id: provider.id, - message: e.message - ) - - provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - - private - - def logger - @logger ||= Gitlab::Kubernetes::Logger.build - end - end - end -end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb deleted file mode 100644 index 73d6fc4dc8f..00000000000 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - class FinalizeCreationService - attr_reader :provider - - def execute(provider) - @provider = provider - - configure_provider - create_gitlab_service_account! - configure_kubernetes - configure_pre_installed_knative if provider.knative_pre_installed? - cluster.save! - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - log_service_error(e.class.name, provider.id, e.message) - provider.make_errored!(s_('ClusterIntegration|Failed to request to Google Cloud Platform: %{message}') % { message: e.message }) - rescue Kubeclient::HttpError => e - log_service_error(e.class.name, provider.id, e.message) - provider.make_errored!(s_('ClusterIntegration|Failed to run Kubeclient: %{message}') % { message: e.message }) - rescue ActiveRecord::RecordInvalid => e - log_service_error(e.class.name, provider.id, e.message) - provider.make_errored!(s_('ClusterIntegration|Failed to configure Google Kubernetes Engine Cluster: %{message}') % { message: e.message }) - end - - private - - def create_gitlab_service_account! - Clusters::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator( - kube_client, - rbac: create_rbac_cluster? - ).execute - end - - def configure_provider - provider.endpoint = gke_cluster.endpoint - provider.status_event = :make_created - end - - def configure_kubernetes - cluster.platform_type = :kubernetes - cluster.build_platform_kubernetes( - api_url: 'https://' + gke_cluster.endpoint, - ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), - authorization_type: authorization_type, - token: request_kubernetes_token) - end - - def configure_pre_installed_knative - knative = cluster.build_application_knative( - hostname: 'example.com' - ) - knative.make_pre_installed! - end - - def request_kubernetes_token - Clusters::Kubernetes::FetchKubernetesTokenService.new( - kube_client, - Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, - Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE - ).execute - end - - def authorization_type - create_rbac_cluster? ? 'rbac' : 'abac' - end - - def create_rbac_cluster? - !provider.legacy_abac? - end - - def kube_client - @kube_client ||= build_kube_client!( - 'https://' + gke_cluster.endpoint, - Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - ) - end - - def build_kube_client!(api_url, ca_pem) - raise "Incomplete settings" unless api_url - - Gitlab::Kubernetes::KubeClient.new( - api_url, - auth_options: { bearer_token: provider.access_token }, - ssl_options: kubeclient_ssl_options(ca_pem), - http_proxy_uri: ENV['http_proxy'] - ) - end - - def kubeclient_ssl_options(ca_pem) - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - - def gke_cluster - @gke_cluster ||= provider.api_client.projects_zones_clusters_get( - provider.gcp_project_id, - provider.zone, - cluster.name) - end - - def cluster - @cluster ||= provider.cluster - end - - def logger - @logger ||= Gitlab::Kubernetes::Logger.build - end - - def log_service_error(exception, provider_id, message) - logger.error( - exception: exception.class.name, - service: self.class.name, - provider_id: provider_id, - message: message - ) - end - end - end -end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb deleted file mode 100644 index 7dc2d3c32f1..00000000000 --- a/app/services/clusters/gcp/provision_service.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - class ProvisionService - CLOUD_RUN_ADDONS = %i[http_load_balancing istio_config cloud_run_config].freeze - - attr_reader :provider - - def execute(provider) - @provider = provider - - get_operation_id do |operation_id| - if provider.make_creating(operation_id) - WaitForClusterCreationWorker.perform_in( - Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL, - provider.cluster_id) - else - provider.make_errored!("Failed to update provider record; #{provider.errors}") - end - end - end - - private - - def get_operation_id - enable_addons = provider.cloud_run? ? CLOUD_RUN_ADDONS : [] - - operation = provider.api_client.projects_zones_clusters_create( - provider.gcp_project_id, - provider.zone, - provider.cluster.name, - provider.num_nodes, - machine_type: provider.machine_type, - legacy_abac: provider.legacy_abac, - enable_addons: enable_addons - ) - - unless operation.status == 'PENDING' || operation.status == 'RUNNING' - return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") - end - - operation_id = provider.api_client.parse_operation_id(operation.self_link) - - unless operation_id - return provider.make_errored!('Can not find operation_id from self_link') - end - - yield(operation_id) - - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - end - end -end diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb deleted file mode 100644 index ddb2832aae6..00000000000 --- a/app/services/clusters/gcp/verify_provision_status_service.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Gcp - class VerifyProvisionStatusService - attr_reader :provider - - INITIAL_INTERVAL = 2.minutes - EAGER_INTERVAL = 10.seconds - TIMEOUT = 20.minutes - - def execute(provider) - @provider = provider - - request_operation do |operation| - case operation.status - when 'PENDING', 'RUNNING' - continue_creation(operation) - when 'DONE' - finalize_creation - else - provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") - end - end - end - - private - - def continue_creation(operation) - if elapsed_time_from_creation(operation) < TIMEOUT - WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) - else - provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT }) - end - end - - def elapsed_time_from_creation(operation) - Time.current.utc - operation.start_time.to_time.utc - end - - def finalize_creation - Clusters::Gcp::FinalizeCreationService.new.execute(provider) - end - - def request_operation(&blk) - Clusters::Gcp::FetchOperationService.new.execute(provider, &blk) - end - end - end -end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 652a0021b0f..5a2303400bd 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -951,11 +951,11 @@ - :name: gcp_cluster:cluster_provision :worker_name: ClusterProvisionWorker :feature_category: :kubernetes_management - :has_external_dependencies: true + :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown :weight: 1 - :idempotent: false + :idempotent: true :tags: [] - :name: gcp_cluster:cluster_update_app :worker_name: ClusterUpdateAppWorker @@ -1059,11 +1059,11 @@ - :name: gcp_cluster:wait_for_cluster_creation :worker_name: WaitForClusterCreationWorker :feature_category: :kubernetes_management - :has_external_dependencies: true + :has_external_dependencies: false :urgency: :low :resource_boundary: :unknown :weight: 1 - :idempotent: false + :idempotent: true :tags: [] - :name: github_gists_importer:github_gists_import_finish_import :worker_name: Gitlab::GithubGistsImport::FinishImportWorker diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 04c9174347f..6f3615d249c 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ClusterProvisionWorker # rubocop:disable Scalability/IdempotentWorker +class ClusterProvisionWorker include ApplicationWorker data_consistency :always @@ -8,17 +8,7 @@ class ClusterProvisionWorker # rubocop:disable Scalability/IdempotentWorker sidekiq_options retry: 3 include ClusterQueue - worker_has_external_dependencies! + idempotent! - def perform(cluster_id) - Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.provider.try do |provider| - if cluster.gcp? - Clusters::Gcp::ProvisionService.new.execute(provider) - elsif cluster.aws? - Clusters::Aws::ProvisionService.new.execute(provider) - end - end - end - end + def perform(_); end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index af351c3c207..a34f5386363 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class WaitForClusterCreationWorker # rubocop:disable Scalability/IdempotentWorker +class WaitForClusterCreationWorker include ApplicationWorker data_consistency :always @@ -8,17 +8,7 @@ class WaitForClusterCreationWorker # rubocop:disable Scalability/IdempotentWorke sidekiq_options retry: 3 include ClusterQueue - worker_has_external_dependencies! + idempotent! - def perform(cluster_id) - Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.provider.try do |provider| - if cluster.gcp? - Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) - elsif cluster.aws? - Clusters::Aws::VerifyProvisionStatusService.new.execute(provider) - end - end - end - end + def perform(_); end end diff --git a/config/feature_flags/development/rate_limit_issuable_searches.yml b/config/feature_flags/development/rate_limit_issuable_searches.yml new file mode 100644 index 00000000000..9a4909da72e --- /dev/null +++ b/config/feature_flags/development/rate_limit_issuable_searches.yml @@ -0,0 +1,8 @@ +--- +name: rate_limit_issuable_searches +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104208" +rollout_issue_url: +milestone: '15.8' +type: development +group: group::project management +default_enabled: false diff --git a/doc/development/backend/create_source_code_be/index.md b/doc/development/backend/create_source_code_be/index.md index 8a1a541fac9..5e087382dbc 100644 --- a/doc/development/backend/create_source_code_be/index.md +++ b/doc/development/backend/create_source_code_be/index.md @@ -48,3 +48,5 @@ For more information, read [Gitaly touch points](gitaly_touch_points.md). Create: Source Code has over 100 REST endpoints, being a mixture of Grape API endpoints and Rails controller endpoints. For a detailed list, refer to [Source Code REST Endpoints](rest_endpoints.md). + +An alternative list of the [Source Code endpoints and other owned objects](https://gitlab-com.gitlab.io/gl-infra/platform/stage-groups-index/source-code.html) is available. diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0b5a471ea12..95c81c14bf9 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -695,6 +695,16 @@ module API unprocessable_entity!('User must be authenticated to use search') end + def validate_search_rate_limit! + return unless Feature.enabled?(:rate_limit_issuable_searches) + + if current_user + check_rate_limit!(:search_rate_limit, scope: [current_user]) + else + check_rate_limit!(:search_rate_limit_unauthenticated, scope: [ip_address]) + end + end + private # rubocop:disable Gitlab/ModuleWithInstanceVariables diff --git a/lib/api/helpers/rate_limiter.rb b/lib/api/helpers/rate_limiter.rb index 03f3cd649b1..be92277c25a 100644 --- a/lib/api/helpers/rate_limiter.rb +++ b/lib/api/helpers/rate_limiter.rb @@ -10,25 +10,14 @@ module API # See app/controllers/concerns/check_rate_limit.rb for Rails controllers version module RateLimiter def check_rate_limit!(key, scope:, **options) - return if bypass_header_set? - return unless rate_limiter.throttled?(key, scope: scope, **options) - - rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) + return unless Gitlab::ApplicationRateLimiter.throttled_request?( + request, current_user, key, scope: scope, **options + ) return yield if block_given? render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429) end - - private - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end - - def bypass_header_set? - ::Gitlab::Throttle.bypass_header.present? && request.get_header(Gitlab::Throttle.bypass_header) == '1' - end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b08819e34e3..7b6306938cf 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -116,6 +116,7 @@ module API get '/issues_statistics' do authenticate! unless params[:scope] == 'all' validate_anonymous_search_access! if params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? present issues_statistics, with: Grape::Presenters::Presenter end @@ -134,6 +135,7 @@ module API get do authenticate! unless params[:scope] == 'all' validate_anonymous_search_access! if params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? issues = paginate(find_issues) options = { @@ -173,6 +175,7 @@ module API end get ":id/issues" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? issues = paginate(find_issues(group_id: user_group.id, include_subgroups: true)) options = { @@ -192,6 +195,7 @@ module API end get ":id/issues_statistics" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? present issues_statistics(group_id: user_group.id, include_subgroups: true), with: Grape::Presenters::Presenter end @@ -211,6 +215,7 @@ module API end get ":id/issues" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? issues = paginate(find_issues(project_id: user_project.id)) options = { @@ -230,6 +235,7 @@ module API end get ":id/issues_statistics" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? present issues_statistics(project_id: user_project.id), with: Grape::Presenters::Presenter end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a9572cf7ce6..8a665b71bdb 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -149,6 +149,7 @@ module API get feature_category: :code_review, urgency: :low do authenticate! unless params[:scope] == 'all' validate_anonymous_search_access! if params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? merge_requests = find_merge_requests present merge_requests, serializer_options_for(merge_requests) @@ -177,6 +178,7 @@ module API end get ":id/merge_requests", feature_category: :code_review, urgency: :low do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true) present merge_requests, serializer_options_for(merge_requests).merge(group: user_group) @@ -244,6 +246,7 @@ module API get ":id/merge_requests", feature_category: :code_review, urgency: :low do authorize! :read_merge_request, user_project validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? merge_requests = find_merge_requests(project_id: user_project.id) diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index 5b1bf99e297..a788586ebec 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -115,6 +115,38 @@ module Gitlab value > threshold_value end + # Similar to #throttled? above but checks for the bypass header in the request and logs the request when it is over the rate limit + # + # @param request [Http::Request] - Web request used to check the header and log + # @param current_user [User] Current user of the request, it can be nil + # @param key [Symbol] Key attribute registered in `.rate_limits` + # @param scope [Array<ActiveRecord>] Array of ActiveRecord models, Strings + # or Symbols to scope throttling to a specific request (e.g. per user + # per project) + # @param resource [ActiveRecord] An ActiveRecord model to count an action + # for (e.g. limit unique project (resource) downloads (action) to five + # per user (scope)) + # @param threshold [Integer] Optional threshold value to override default + # one registered in `.rate_limits` + # @param interval [Integer] Optional interval value to override default + # one registered in `.rate_limits` + # @param users_allowlist [Array<String>] Optional list of usernames to + # exclude from the limit. This param will only be functional if Scope + # includes a current user. + # @param peek [Boolean] Optional. When true the key will not be + # incremented but the current throttled state will be returned. + # + # @return [Boolean] Whether or not a request should be throttled + def throttled_request?(request, current_user, key, scope:, **options) + if ::Gitlab::Throttle.bypass_header.present? && request.get_header(Gitlab::Throttle.bypass_header) == '1' + return false + end + + throttled?(key, scope: scope, **options).tap do |throttled| + log_request(request, "#{key}_request_limit".to_sym, current_user) if throttled + end + end + # Returns the current rate limited state without incrementing the count. # # @param key [Symbol] Key attribute registered in `.rate_limits` diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aec6f2944b1..177789089cb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1927,9 +1927,6 @@ msgstr "" msgid "AWS secret access key (Optional)" msgstr "" -msgid "AWS service error: %{error}" -msgstr "" - msgid "Abort" msgstr "" @@ -1978,9 +1975,6 @@ msgstr "" msgid "Access denied for your LDAP account." msgstr "" -msgid "Access denied: %{error}" -msgstr "" - msgid "Access expires" msgstr "" @@ -4261,9 +4255,6 @@ msgstr "" msgid "An error occurred while approving, please try again." msgstr "" -msgid "An error occurred while authorizing your role" -msgstr "" - msgid "An error occurred while checking group path. Please refresh and try again." msgstr "" @@ -9580,21 +9571,6 @@ msgstr "" msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration." msgstr "" -msgid "ClusterIntegration|Failed to configure EKS provider: %{message}" -msgstr "" - -msgid "ClusterIntegration|Failed to configure Google Kubernetes Engine Cluster: %{message}" -msgstr "" - -msgid "ClusterIntegration|Failed to fetch CloudFormation stack: %{message}" -msgstr "" - -msgid "ClusterIntegration|Failed to request to Google Cloud Platform: %{message}" -msgstr "" - -msgid "ClusterIntegration|Failed to run Kubeclient: %{message}" -msgstr "" - msgid "ClusterIntegration|GitLab Integration" msgstr "" @@ -16071,21 +16047,12 @@ msgstr "" msgid "Error: Gitaly is unavailable. Contact your administrator." msgstr "" -msgid "Error: No AWS credentials were supplied" -msgstr "" - -msgid "Error: No AWS provision role found for user" -msgstr "" - msgid "Error: Unable to create deploy freeze" msgstr "" msgid "Error: Unable to delete deploy freeze" msgstr "" -msgid "Error: Unable to find AWS role for current user" -msgstr "" - msgid "ErrorTracking|Access token is %{token_in_code_tag}" msgstr "" @@ -24106,9 +24073,6 @@ msgstr "" msgid "Kubernetes cluster" msgstr "" -msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}" -msgstr "" - msgid "Kubernetes cluster integration and resources are being removed." msgstr "" diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index c432adb6ae3..86a4ac61194 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -159,8 +159,6 @@ RSpec.describe Admin::ClustersController do describe 'functionality' do context 'when creates a cluster' do it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { post_create_user }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } @@ -187,8 +185,6 @@ RSpec.describe Admin::ClustersController do end it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { post_create_user }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } diff --git a/spec/controllers/concerns/check_rate_limit_spec.rb b/spec/controllers/concerns/check_rate_limit_spec.rb index 75776acd520..25574aa295b 100644 --- a/spec/controllers/concerns/check_rate_limit_spec.rb +++ b/spec/controllers/concerns/check_rate_limit_spec.rb @@ -33,8 +33,8 @@ RSpec.describe CheckRateLimit do end describe '#check_rate_limit!' do - it 'calls ApplicationRateLimiter#throttled? with the right arguments' do - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false) + it 'calls ApplicationRateLimiter#throttled_request? with the right arguments' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled_request?).with(request, user, key, scope: scope).and_return(false) expect(subject).not_to receive(:render) subject.check_rate_limit!(key, scope: scope) diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index eb3fe4bc330..46f507c34ba 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -180,8 +180,6 @@ RSpec.describe Groups::ClustersController do describe 'functionality' do context 'when creates a cluster' do it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } @@ -210,8 +208,6 @@ RSpec.describe Groups::ClustersController do end it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 12202518e1e..894f0f8354d 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -181,8 +181,6 @@ RSpec.describe Projects::ClustersController do describe 'functionality' do context 'when creates a cluster' do it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } @@ -210,8 +208,6 @@ RSpec.describe Projects::ClustersController do end it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js index 7a9262cd004..88460221168 100644 --- a/spec/frontend/autosave_spec.js +++ b/spec/frontend/autosave_spec.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import Autosave from '~/autosave'; import AccessorUtilities from '~/lib/utils/accessor'; @@ -7,12 +6,19 @@ describe('Autosave', () => { useLocalStorageSpy(); let autosave; - const field = $('<textarea></textarea>'); - const checkbox = $('<input type="checkbox">'); + const field = document.createElement('textarea'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; const key = 'key'; const fallbackKey = 'fallbackKey'; const lockVersionKey = 'lockVersionKey'; const lockVersion = 1; + const getAutosaveKey = () => `autosave/${key}`; + const getAutosaveLockKey = () => `autosave/${key}/lockVersion`; + + afterEach(() => { + autosave?.dispose?.(); + }); describe('class constructor', () => { beforeEach(() => { @@ -43,18 +49,10 @@ describe('Autosave', () => { }); describe('restore', () => { - beforeEach(() => { - autosave = { - field, - key, - }; - }); - describe('if .isLocalStorageAvailable is `false`', () => { beforeEach(() => { - autosave.isLocalStorageAvailable = false; - - Autosave.prototype.restore.call(autosave); + jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false); + autosave = new Autosave(field, key); }); it('should not call .getItem', () => { @@ -63,97 +61,73 @@ describe('Autosave', () => { }); describe('if .isLocalStorageAvailable is `true`', () => { - beforeEach(() => { - autosave.isLocalStorageAvailable = true; - }); - it('should call .getItem', () => { - Autosave.prototype.restore.call(autosave); - - expect(window.localStorage.getItem).toHaveBeenCalledWith(key); + autosave = new Autosave(field, key); + expect(window.localStorage.getItem.mock.calls).toEqual([[getAutosaveKey()], []]); }); - it('triggers jquery event', () => { - jest.spyOn(autosave.field, 'trigger').mockImplementation(() => {}); - - Autosave.prototype.restore.call(autosave); - - expect(field.trigger).toHaveBeenCalled(); - }); - - it('triggers native event', () => { - const fieldElement = autosave.field.get(0); - const eventHandler = jest.fn(); - fieldElement.addEventListener('change', eventHandler); - - Autosave.prototype.restore.call(autosave); + describe('if saved value is present', () => { + const storedValue = 'bar'; - expect(eventHandler).toHaveBeenCalledTimes(1); - fieldElement.removeEventListener('change', eventHandler); - }); - - describe('if field type is checkbox', () => { beforeEach(() => { - autosave = { - field: checkbox, - key, - isLocalStorageAvailable: true, - type: 'checkbox', - }; + field.value = 'foo'; + window.localStorage.setItem(getAutosaveKey(), storedValue); }); - it('should restore', () => { - window.localStorage.setItem(key, true); - expect(checkbox.is(':checked')).toBe(false); - Autosave.prototype.restore.call(autosave); - expect(checkbox.is(':checked')).toBe(true); + it('restores the value', () => { + autosave = new Autosave(field, key); + expect(field.value).toEqual(storedValue); }); - }); - }); - describe('if field gets deleted from DOM', () => { - beforeEach(() => { - autosave.field = $('.not-a-real-element'); - }); + it('triggers native event', () => { + const eventHandler = jest.fn(); + field.addEventListener('change', eventHandler); + autosave = new Autosave(field, key); - it('does not trigger event', () => { - jest.spyOn(field, 'trigger'); + expect(eventHandler).toHaveBeenCalledTimes(1); + field.removeEventListener('change', eventHandler); + }); + + describe('if field type is checkbox', () => { + beforeEach(() => { + checkbox.checked = false; + window.localStorage.setItem(getAutosaveKey(), true); + autosave = new Autosave(checkbox, key); + }); - expect(field.trigger).not.toHaveBeenCalled(); + it('should restore', () => { + expect(checkbox.checked).toBe(true); + }); + }); }); }); }); describe('getSavedLockVersion', () => { - beforeEach(() => { - autosave = { - field, - key, - lockVersionKey, - }; - }); - describe('if .isLocalStorageAvailable is `false`', () => { beforeEach(() => { - autosave.isLocalStorageAvailable = false; - - Autosave.prototype.getSavedLockVersion.call(autosave); + jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false); + autosave = new Autosave(field, key); }); it('should not call .getItem', () => { + autosave.getSavedLockVersion(); expect(window.localStorage.getItem).not.toHaveBeenCalled(); }); }); describe('if .isLocalStorageAvailable is `true`', () => { beforeEach(() => { - autosave.isLocalStorageAvailable = true; + autosave = new Autosave(field, key); }); it('should call .getItem', () => { - Autosave.prototype.getSavedLockVersion.call(autosave); - - expect(window.localStorage.getItem).toHaveBeenCalledWith(lockVersionKey); + autosave.getSavedLockVersion(); + expect(window.localStorage.getItem.mock.calls).toEqual([ + [getAutosaveKey()], + [], + [getAutosaveLockKey()], + ]); }); }); }); @@ -162,7 +136,7 @@ describe('Autosave', () => { beforeEach(() => { autosave = { reset: jest.fn() }; autosave.field = field; - field.val('value'); + field.value = 'value'; }); describe('if .isLocalStorageAvailable is `false`', () => { @@ -200,14 +174,14 @@ describe('Autosave', () => { }); it('should save true when checkbox on', () => { - checkbox.prop('checked', true); + checkbox.checked = true; Autosave.prototype.save.call(autosave); expect(window.localStorage.setItem).toHaveBeenCalledWith(key, true); }); it('should call reset when checkbox off', () => { autosave.reset = jest.fn(); - checkbox.prop('checked', false); + checkbox.checked = false; Autosave.prototype.save.call(autosave); expect(autosave.reset).toHaveBeenCalled(); expect(window.localStorage.setItem).not.toHaveBeenCalled(); diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js index 462ef7e7280..003a6d86371 100644 --- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js @@ -3,6 +3,8 @@ import Vuex from 'vuex'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue'; +jest.mock('~/autosave'); + Vue.use(Vuex); let wrapper; diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index 5fd61b25edc..f4d4f9cf896 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -5,6 +5,7 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); +jest.mock('~/autosave'); describe('Design reply form component', () => { let wrapper; @@ -78,12 +79,11 @@ describe('Design reply form component', () => { createComponent({ discussionId }); await nextTick(); - // We discourage testing `wrapper.vm` properties but - // since `autosave` library instantiates on component - // there's no other way to test whether instantiation - // happened correctly or not. - expect(wrapper.vm.autosaveDiscussion).toBeInstanceOf(Autosave); - expect(wrapper.vm.autosaveDiscussion.key).toBe(`autosave/Discussion/6/${shortDiscussionId}`); + expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [ + 'Discussion', + 6, + shortDiscussionId, + ]); }, ); @@ -141,7 +141,7 @@ describe('Design reply form component', () => { }); it('emits submitForm event on Comment button click', async () => { - const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); findSubmitButton().vm.$emit('click'); @@ -151,7 +151,7 @@ describe('Design reply form component', () => { }); it('emits submitForm event on textarea ctrl+enter keydown', async () => { - const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); findTextarea().trigger('keydown.enter', { ctrlKey: true, @@ -163,7 +163,7 @@ describe('Design reply form component', () => { }); it('emits submitForm event on textarea meta+enter keydown', async () => { - const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); findTextarea().trigger('keydown.enter', { metaKey: true, @@ -178,7 +178,7 @@ describe('Design reply form component', () => { findTextarea().setValue('test2'); await nextTick(); - expect(wrapper.emitted('input')).toEqual([['test'], ['test2']]); + expect(wrapper.emitted('input')).toEqual([['test2']]); }); it('emits cancelForm event on Escape key if text was not changed', () => { @@ -211,7 +211,7 @@ describe('Design reply form component', () => { it('emits cancelForm event when confirmed', async () => { confirmAction.mockResolvedValueOnce(true); - const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); wrapper.setProps({ value: 'test3' }); await nextTick(); @@ -228,7 +228,7 @@ describe('Design reply form component', () => { it("doesn't emit cancelForm event when not confirmed", async () => { confirmAction.mockResolvedValueOnce(false); - const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset'); wrapper.setProps({ value: 'test3' }); await nextTick(); diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js index 9493dc8855e..bd0e3455872 100644 --- a/spec/frontend/diffs/components/diff_line_note_form_spec.js +++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js @@ -101,7 +101,8 @@ describe('DiffLineNoteForm', () => { }); it('should init autosave', () => { - expect(Autosave).toHaveBeenCalledWith({}, [ + // we're using shallow mount here so there's no element to pass to Autosave + expect(Autosave).toHaveBeenCalledWith(undefined, [ 'Note', 'Issue', 98, diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js index 5e67ea42b87..28ec0e22d8b 100644 --- a/spec/frontend/issuable/issuable_form_spec.js +++ b/spec/frontend/issuable/issuable_form_spec.js @@ -35,8 +35,8 @@ describe('IssuableForm', () => { let $description; beforeEach(() => { - $title = $form.find('input[name*="[title]"]'); - $description = $form.find('textarea[name*="[description]"]'); + $title = $form.find('input[name*="[title]"]').get(0); + $description = $form.find('textarea[name*="[description]"]').get(0); }); afterEach(() => { @@ -103,7 +103,11 @@ describe('IssuableForm', () => { createIssuable($form); expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields); - expect(Autosave).toHaveBeenLastCalledWith($input, ['/', '', id], `autosave///=${id}`); + expect(Autosave).toHaveBeenLastCalledWith( + $input.get(0), + ['/', '', id], + `autosave///=${id}`, + ); }); }); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 701ff492702..e13985ef469 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import Autosave from '~/autosave'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { createAlert } from '~/flash'; @@ -20,6 +21,7 @@ import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } jest.mock('autosize'); jest.mock('~/commons/nav/user_merge_requests'); jest.mock('~/flash'); +jest.mock('~/autosave'); Vue.use(Vuex); @@ -336,8 +338,11 @@ describe('issue_comment_form component', () => { }); it('inits autosave', () => { - expect(wrapper.vm.autosave).toBeDefined(); - expect(wrapper.vm.autosave.key).toBe(`autosave/Note/Issue/${noteableDataMock.id}`); + expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [ + 'Note', + 'Issue', + noteableDataMock.id, + ]); }); }); diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index 3b5313744ff..c71cf7666ab 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -7,11 +7,14 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue'; import NoteForm from '~/notes/components/note_form.vue'; import createStore from '~/notes/stores'; import notes from '~/notes/stores/modules/index'; +import Autosave from '~/autosave'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; +jest.mock('~/autosave'); + const createComponent = ({ props = {}, noteableData = noteableDataMock, @@ -84,13 +87,8 @@ describe('issue_note_body component', () => { }); it('adds autosave', () => { - const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`; - - // While we discourage testing wrapper props - // here we aren't testing a component prop - // but instead an instance object property - // which is defined in `app/assets/javascripts/notes/mixins/autosave.js` - expect(wrapper.vm.autosave.key).toEqual(autosaveKey); + // passing undefined instead of an element because of shallowMount + expect(Autosave).toHaveBeenCalledWith(undefined, ['Note', note.noteable_type, note.id]); }); describe('isInternalNote', () => { diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js index cb7262b15e3..2b62cbb9ab3 100644 --- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js +++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js @@ -1,11 +1,12 @@ import { shallowMount } from '@vue/test-utils'; -import { GlListbox } from '@gitlab/ui'; +import { GlFormGroup, GlListbox } from '@gitlab/ui'; import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue'; describe('ListboxInput', () => { let wrapper; // Props + const label = 'label'; const name = 'name'; const defaultToggleText = 'defaultToggleText'; const items = [ @@ -23,12 +24,14 @@ describe('ListboxInput', () => { ]; // Finders + const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); const findGlListbox = () => wrapper.findComponent(GlListbox); const findInput = () => wrapper.find('input'); const createComponent = (propsData) => { wrapper = shallowMount(ListboxInput, { propsData: { + label, name, defaultToggleText, items, @@ -37,14 +40,22 @@ describe('ListboxInput', () => { }); }; - describe('input attributes', () => { + describe('options', () => { beforeEach(() => { createComponent(); }); + it('passes the label to the form group', () => { + expect(findGlFormGroup().attributes('label')).toBe(label); + }); + it('sets the input name', () => { expect(findInput().attributes('name')).toBe(name); }); + + it('is not filterable with few items', () => { + expect(findGlListbox().props('searchable')).toBe(false); + }); }); describe('toggle text', () => { @@ -91,12 +102,29 @@ describe('ListboxInput', () => { }); describe('search', () => { - beforeEach(() => { - createComponent(); + it('is searchable when there are more than 10 items', () => { + createComponent({ + items: [ + { + text: 'Group 1', + options: [...Array(10).keys()].map((index) => ({ + text: index + 1, + value: String(index + 1), + })), + }, + { + text: 'Group 2', + options: [{ text: 'Item 11', value: '11' }], + }, + ], + }); + + expect(findGlListbox().props('searchable')).toBe(true); }); it('passes all items to GlListbox by default', () => { createComponent(); + expect(findGlListbox().props('items')).toStrictEqual(items); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 33f370efdfa..5461d38599d 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -90,6 +90,17 @@ describe('Source Viewer component', () => { }); }); + describe('legacy fallbacks', () => { + it('tracks a fallback event and emits an error when viewing python files', () => { + const fallbackLanguage = 'python'; + const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage }; + createComponent({ language: fallbackLanguage }); + + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + expect(wrapper.emitted('error')).toHaveLength(1); + }); + }); + describe('highlight.js', () => { beforeEach(() => createComponent({ language: mappedLanguage })); @@ -114,10 +125,10 @@ describe('Source Viewer component', () => { }); it('correctly maps languages starting with uppercase', async () => { - await createComponent({ language: 'Python3' }); - const languageDefinition = await import(`highlight.js/lib/languages/python`); + await createComponent({ language: 'Ruby' }); + const languageDefinition = await import(`highlight.js/lib/languages/ruby`); - expect(hljs.registerLanguage).toHaveBeenCalledWith('python', languageDefinition.default); + expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default); }); it('highlights the first chunk', () => { diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index e5594b6d37e..159be4cd1ef 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -5,9 +5,12 @@ import { nextTick } from 'vue'; import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue'; import IssuableEventHub from '~/vue_shared/issuable/show/event_hub'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import Autosave from '~/autosave'; import { mockIssuableShowProps, mockIssuable } from '../mock_data'; +jest.mock('~/autosave'); + const issuableEditFormProps = { issuable: mockIssuable, ...mockIssuableShowProps, @@ -36,10 +39,12 @@ describe('IssuableEditForm', () => { beforeEach(() => { wrapper = createComponent(); + jest.spyOn(Autosave.prototype, 'reset'); }); afterEach(() => { wrapper.destroy(); + jest.resetAllMocks(); }); describe('watch', () => { @@ -100,21 +105,18 @@ describe('IssuableEditForm', () => { describe('methods', () => { describe('initAutosave', () => { - it('initializes `autosaveTitle` and `autosaveDescription` props', () => { - expect(wrapper.vm.autosaveTitle).toBeDefined(); - expect(wrapper.vm.autosaveDescription).toBeDefined(); + it('initializes autosave', () => { + expect(Autosave.mock.calls).toEqual([ + [expect.any(Element), ['/', '', 'title']], + [expect.any(Element), ['/', '', 'description']], + ]); }); }); describe('resetAutosave', () => { - it('calls `reset` on `autosaveTitle` and `autosaveDescription` props', () => { - jest.spyOn(wrapper.vm.autosaveTitle, 'reset').mockImplementation(jest.fn); - jest.spyOn(wrapper.vm.autosaveDescription, 'reset').mockImplementation(jest.fn); - - wrapper.vm.resetAutosave(); - - expect(wrapper.vm.autosaveTitle.reset).toHaveBeenCalled(); - expect(wrapper.vm.autosaveDescription.reset).toHaveBeenCalled(); + it('resets title and description on "update.issuable event"', () => { + IssuableEventHub.$emit('update.issuable'); + expect(Autosave.prototype.reset.mock.calls).toEqual([[], []]); }); }); }); diff --git a/spec/lib/api/helpers/rate_limiter_spec.rb b/spec/lib/api/helpers/rate_limiter_spec.rb index 3640c7e30e7..531140a32a3 100644 --- a/spec/lib/api/helpers/rate_limiter_spec.rb +++ b/spec/lib/api/helpers/rate_limiter_spec.rb @@ -31,8 +31,8 @@ RSpec.describe API::Helpers::RateLimiter do end describe '#check_rate_limit!' do - it 'calls ApplicationRateLimiter#throttled? with the right arguments' do - expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false) + it 'calls ApplicationRateLimiter#throttled_request? with the right arguments' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled_request?).with(request, user, key, scope: scope).and_return(false) expect(subject).not_to receive(:render_api_error!) subject.check_rate_limit!(key, scope: scope) diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb index 41e79f811fa..c938393adce 100644 --- a/spec/lib/gitlab/application_rate_limiter_spec.rb +++ b/spec/lib/gitlab/application_rate_limiter_spec.rb @@ -214,6 +214,52 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting end end + describe '.throttled_request?', :freeze_time do + let(:request) { instance_double('Rack::Request') } + + context 'when request is not over the limit' do + it 'returns false and does not log the request' do + expect(subject).not_to receive(:log_request) + + expect(subject.throttled_request?(request, user, :test_action, scope: [user])).to eq(false) + end + end + + context 'when request is over the limit' do + before do + subject.throttled?(:test_action, scope: [user]) + end + + it 'returns true and logs the request' do + expect(subject).to receive(:log_request).with(request, :test_action_request_limit, user) + + expect(subject.throttled_request?(request, user, :test_action, scope: [user])).to eq(true) + end + + context 'when the bypass header is set' do + before do + allow(Gitlab::Throttle).to receive(:bypass_header).and_return('SOME_HEADER') + end + + it 'skips rate limit if set to "1"' do + allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('1') + + expect(subject).not_to receive(:log_request) + + expect(subject.throttled_request?(request, user, :test_action, scope: [user])).to eq(false) + end + + it 'does not skip rate limit if set to something else than "1"' do + allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('0') + + expect(subject).to receive(:log_request).with(request, :test_action_request_limit, user) + + expect(subject.throttled_request?(request, user, :test_action, scope: [user])).to eq(true) + end + end + end + end + describe '.peek' do it 'peeks at the current state without changing its value' do freeze_time do diff --git a/spec/models/clusters/providers/aws_spec.rb b/spec/models/clusters/providers/aws_spec.rb index 2afed663edf..cb2960e1557 100644 --- a/spec/models/clusters/providers/aws_spec.rb +++ b/spec/models/clusters/providers/aws_spec.rb @@ -75,39 +75,6 @@ RSpec.describe Clusters::Providers::Aws do end end - describe '#api_client' do - let(:provider) { create(:cluster_provider_aws) } - let(:credentials) { double } - let(:client) { double } - - subject { provider.api_client } - - before do - allow(provider).to receive(:credentials).and_return(credentials) - - expect(Aws::CloudFormation::Client).to receive(:new) - .with(credentials: credentials, region: provider.region) - .and_return(client) - end - - it { is_expected.to eq client } - end - - describe '#credentials' do - let(:provider) { create(:cluster_provider_aws) } - let(:credentials) { double } - - subject { provider.credentials } - - before do - expect(Aws::Credentials).to receive(:new) - .with(provider.access_key_id, provider.secret_access_key, provider.session_token) - .and_return(credentials) - end - - it { is_expected.to eq credentials } - end - describe '#created_by_user' do let(:provider) { create(:cluster_provider_aws) } diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb index a1f00069937..afd5699091a 100644 --- a/spec/models/clusters/providers/gcp_spec.rb +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -111,31 +111,6 @@ RSpec.describe Clusters::Providers::Gcp do end end - describe '#api_client' do - subject { gcp.api_client } - - context 'when status is creating' do - let(:gcp) { build(:cluster_provider_gcp, :creating) } - - it 'returns Cloud Platform API clinet' do - expect(subject).to be_an_instance_of(GoogleApi::CloudPlatform::Client) - expect(subject.access_token).to eq(gcp.access_token) - end - end - - context 'when status is created' do - let(:gcp) { build(:cluster_provider_gcp, :created) } - - it { is_expected.to be_nil } - end - - context 'when status is errored' do - let(:gcp) { build(:cluster_provider_gcp, :errored) } - - it { is_expected.to be_nil } - end - end - describe '#nullify_credentials' do let(:provider) { create(:cluster_provider_gcp, :creating) } diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 7a1dc614dcf..131cdb77107 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -88,10 +88,10 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati build_stage = create(:ci_stage, position: 2, name: 'build', project: project, pipeline: pipeline) test_stage = create(:ci_stage, position: 3, name: 'test', project: project, pipeline: pipeline) - create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, stage: build_stage, stage_idx: build_stage.position) - create(:ci_build, pipeline: pipeline, name: 'docker 2 2', stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) - create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', scheduling_type: :stage, stage: test_stage, stage_idx: test_stage.position) - test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', scheduling_type: :dag, stage: test_stage, stage_idx: test_stage.position) + create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, ci_stage: build_stage, stage_idx: build_stage.position) + create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) + create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', scheduling_type: :stage, ci_stage: test_stage, stage_idx: test_stage.position) + test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', scheduling_type: :dag, ci_stage: test_stage, stage_idx: test_stage.position) create(:ci_build_need, build: test_job, name: 'my test job') end diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb index ba6f8ec2cab..145e57bc6ae 100644 --- a/spec/requests/api/graphql/issues_spec.rb +++ b/spec/requests/api/graphql/issues_spec.rb @@ -177,6 +177,32 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl end end + context 'with rate limiting' do + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit, graphql: true do + let_it_be(:current_user) { developer } + + let(:error_message) do + 'This endpoint has been requested with the search argument too many times. Try again later.' + end + + def request + post_graphql(query({ search: 'test' }), current_user: developer) + end + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated, graphql: true do + let_it_be(:current_user) { nil } + + let(:error_message) do + 'This endpoint has been requested with the search argument too many times. Try again later.' + end + + def request + post_graphql(query({ search: 'test' })) + end + end + end + def execute_query post_query end diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 70966d23576..6fc3903103b 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -11,15 +11,24 @@ RSpec.describe API::Issues, feature_category: :team_planning do let_it_be(:group) { create(:group, :public) } - let(:user2) { create(:user) } - let(:non_member) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:non_member) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:author) { create(:author) } let_it_be(:assignee) { create(:assignee) } - let(:admin) { create(:user, :admin) } - let(:issue_title) { 'foo' } - let(:issue_description) { 'closed' } - let!(:closed_issue) do + let_it_be(:admin) { create(:user, :admin) } + + let_it_be(: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(:no_milestone_title) { 'None' } + let(:any_milestone_title) { 'Any' } + + let_it_be(:issue_title) { 'foo' } + let_it_be(:issue_description) { 'closed' } + let_it_be(:closed_issue) do create :closed_issue, author: user, assignees: [user], @@ -31,7 +40,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do closed_at: 1.hour.ago end - let!(:confidential_issue) do + let_it_be(:confidential_issue) do create :issue, :confidential, project: project, @@ -41,7 +50,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do updated_at: 2.hours.ago end - let!(:issue) do + let_it_be(:issue) do create :issue, author: user, assignees: [user], @@ -53,22 +62,12 @@ RSpec.describe API::Issues, feature_category: :team_planning do description: issue_description end - let_it_be(:label) do - create(:label, title: 'label', color: '#FFAABB', project: project) - end + let_it_be(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) } + let_it_be(:label_link) { create(:label_link, label: label, target: issue) } - 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_it_be(: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!(:merge_request1) do + let_it_be(:merge_request1) do create(:merge_request, :simple, author: user, @@ -77,7 +76,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do description: "closes #{issue.to_reference}") end - let!(:merge_request2) do + let_it_be(:merge_request2) do create(:merge_request, :simple, author: user, @@ -101,7 +100,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do shared_examples 'project issues statistics' do it 'returns project issues statistics' do - get api("/issues_statistics", user), params: params + get api("/projects/#{project.id}/issues_statistics", current_user), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['statistics']).not_to be_nil @@ -138,6 +137,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'issues_statistics' do + let(:current_user) { nil } + context 'no state is treated as all state' do let(:params) { {} } let(:counts) { { all: 2, closed: 1, opened: 1 } } @@ -534,30 +535,32 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'issues_statistics' do + let(:current_user) { user } + context 'no state is treated as all state' do let(:params) { {} } - let(:counts) { { all: 2, closed: 1, opened: 1 } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } it_behaves_like 'project issues statistics' end context 'statistics when all state is passed' do let(:params) { { state: :all } } - let(:counts) { { all: 2, closed: 1, opened: 1 } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } it_behaves_like 'project issues statistics' end context 'closed state is treated as all state' do let(:params) { { state: :closed } } - let(:counts) { { all: 2, closed: 1, opened: 1 } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } it_behaves_like 'project issues statistics' end context 'opened state is treated as all state' do let(:params) { { state: :opened } } - let(:counts) { { all: 2, closed: 1, opened: 1 } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } it_behaves_like 'project issues statistics' end @@ -592,7 +595,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'sort does not affect statistics ' do let(:params) { { state: :opened, order_by: 'updated_at' } } - let(:counts) { { all: 2, closed: 1, opened: 1 } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } it_behaves_like 'project issues statistics' end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 94f0443e14a..b89db82b150 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -145,6 +145,11 @@ RSpec.describe API::Issues, feature_category: :team_planning do let(:result) { issuable.id } end + it_behaves_like 'issuable API rate-limited search' do + let(:url) { '/issues' } + let(:issuable) { issue } + end + it 'returns authentication error without any scope' do get api('/issues') diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0b69000ae7e..4cd93603c31 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -55,6 +55,11 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do let(:issuable) { merge_request } let(:result) { [merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id] } end + + it_behaves_like 'issuable API rate-limited search' do + let(:url) { endpoint_path } + let(:issuable) { merge_request } + end end context 'when authenticated' do @@ -663,6 +668,11 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do let(:result) { [merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id] } end + it_behaves_like 'issuable API rate-limited search' do + let(:url) { '/merge_requests' } + let(:issuable) { merge_request } + end + it "returns authentication error without any scope" do get api("/merge_requests") diff --git a/spec/requests/dashboard_controller_spec.rb b/spec/requests/dashboard_controller_spec.rb index 9edacb27c93..1c8ab843ebe 100644 --- a/spec/requests/dashboard_controller_spec.rb +++ b/spec/requests/dashboard_controller_spec.rb @@ -12,4 +12,32 @@ RSpec.describe DashboardController, feature_category: :authentication_and_author let(:url) { issues_dashboard_url(:ics, assignee_username: user.username) } end end + + context 'issues dashboard' do + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + let_it_be(:current_user) { create(:user) } + + before do + sign_in current_user + end + + def request + get issues_dashboard_path, params: { scope: 'all', search: 'test' } + end + end + end + + context 'merge requests dashboard' do + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + let_it_be(:current_user) { create(:user) } + + before do + sign_in current_user + end + + def request + get merge_requests_dashboard_path, params: { scope: 'all', search: 'test' } + end + end + end end diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb index bbf200eaacd..a943bd6449c 100644 --- a/spec/requests/projects/issues_controller_spec.rb +++ b/spec/requests/projects/issues_controller_spec.rb @@ -32,6 +32,28 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do end end + describe 'GET #index.json' do + let_it_be(:public_project) { create(:project, :public) } + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + let_it_be(:current_user) { create(:user) } + + before do + sign_in current_user + end + + def request + get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' } + end + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated do + def request + get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' } + end + end + end + describe 'GET #discussions' do before do login_as(user) diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb index f5f8b5c2d83..aa7a32ef2cf 100644 --- a/spec/requests/projects/merge_requests_controller_spec.rb +++ b/spec/requests/projects/merge_requests_controller_spec.rb @@ -19,6 +19,28 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code end end + describe 'GET #index' do + let_it_be(:public_project) { create(:project, :public) } + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + let_it_be(:current_user) { user } + + before do + sign_in current_user + end + + def request + get project_merge_requests_path(public_project), params: { scope: 'all', search: 'test' } + end + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated do + def request + get project_merge_requests_path(public_project), params: { scope: 'all', search: 'test' } + end + end + end + describe 'GET #discussions' do let_it_be(:discussion) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) } let_it_be(:discussion_reply) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: discussion) } diff --git a/spec/services/clusters/aws/authorize_role_service_spec.rb b/spec/services/clusters/aws/authorize_role_service_spec.rb deleted file mode 100644 index 17bbc372675..00000000000 --- a/spec/services/clusters/aws/authorize_role_service_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Aws::AuthorizeRoleService do - subject { described_class.new(user, params: params).execute } - - let(:role) { create(:aws_role) } - let(:user) { role.user } - let(:credentials) { instance_double(Aws::Credentials) } - let(:credentials_service) { instance_double(Clusters::Aws::FetchCredentialsService, execute: credentials) } - - let(:role_arn) { 'arn:my-role' } - let(:region) { 'region' } - let(:params) do - params = ActionController::Parameters.new({ - cluster: { - role_arn: role_arn, - region: region - } - }) - - params.require(:cluster).permit(:role_arn, :region) - end - - before do - allow(Clusters::Aws::FetchCredentialsService).to receive(:new) - .with(instance_of(Aws::Role)).and_return(credentials_service) - end - - context 'role exists' do - it 'updates the existing Aws::Role record and returns a set of credentials' do - expect(subject.status).to eq(:ok) - expect(subject.body).to eq(credentials) - expect(role.reload.role_arn).to eq(role_arn) - end - end - - context 'errors' do - shared_examples 'bad request' do - it 'returns an empty hash' do - expect(subject.status).to eq(:unprocessable_entity) - expect(subject.body).to eq({ message: message }) - end - - it 'logs the error' do - expect(::Gitlab::ErrorTracking).to receive(:track_exception) - - subject - end - end - - context 'role does not exist' do - let(:user) { create(:user) } - let(:message) { 'Error: Unable to find AWS role for current user' } - - include_examples 'bad request' - end - - context 'supplied ARN is invalid' do - let(:role_arn) { 'invalid' } - let(:message) { 'Validation failed: Role arn must be a valid Amazon Resource Name' } - - include_examples 'bad request' - end - - context 'client errors' do - before do - allow(credentials_service).to receive(:execute).and_raise(error) - end - - context 'error fetching credentials' do - let(:error) { Aws::STS::Errors::ServiceError.new(nil, 'error message') } - let(:message) { 'AWS service error: error message' } - - include_examples 'bad request' - end - - context 'error in assuming role' do - let(:raw_message) { "User foo is not authorized to perform: sts:AssumeRole on resource bar" } - let(:error) { Aws::STS::Errors::AccessDenied.new(nil, raw_message) } - let(:message) { "Access denied: #{raw_message}" } - - include_examples 'bad request' - end - - context 'credentials not configured' do - let(:error) { Aws::Errors::MissingCredentialsError.new('error message') } - let(:message) { "Error: No AWS credentials were supplied" } - - include_examples 'bad request' - end - - context 'role not configured' do - let(:error) { Clusters::Aws::FetchCredentialsService::MissingRoleError.new('error message') } - let(:message) { "Error: No AWS provision role found for user" } - - include_examples 'bad request' - end - end - end -end diff --git a/spec/services/clusters/aws/fetch_credentials_service_spec.rb b/spec/services/clusters/aws/fetch_credentials_service_spec.rb deleted file mode 100644 index 0358ca1f535..00000000000 --- a/spec/services/clusters/aws/fetch_credentials_service_spec.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Aws::FetchCredentialsService do - describe '#execute' do - let(:user) { create(:user) } - let(:provider) { create(:cluster_provider_aws, region: 'ap-southeast-2') } - - let(:gitlab_access_key_id) { 'gitlab-access-key-id' } - let(:gitlab_secret_access_key) { 'gitlab-secret-access-key' } - - let(:gitlab_credentials) { Aws::Credentials.new(gitlab_access_key_id, gitlab_secret_access_key) } - let(:sts_client) { Aws::STS::Client.new(credentials: gitlab_credentials, region: region) } - let(:assumed_role) { instance_double(Aws::AssumeRoleCredentials, credentials: assumed_role_credentials) } - - let(:assumed_role_credentials) { double } - - subject { described_class.new(provision_role, provider: provider).execute } - - context 'provision role is configured' do - let(:provision_role) { create(:aws_role, user: user, region: 'custom-region') } - - before do - stub_application_setting(eks_access_key_id: gitlab_access_key_id) - stub_application_setting(eks_secret_access_key: gitlab_secret_access_key) - - expect(Aws::Credentials).to receive(:new) - .with(gitlab_access_key_id, gitlab_secret_access_key) - .and_return(gitlab_credentials) - - expect(Aws::STS::Client).to receive(:new) - .with(credentials: gitlab_credentials, region: region) - .and_return(sts_client) - - expect(Aws::AssumeRoleCredentials).to receive(:new) - .with( - client: sts_client, - role_arn: provision_role.role_arn, - role_session_name: session_name, - external_id: provision_role.role_external_id, - policy: session_policy - ).and_return(assumed_role) - end - - context 'provider is specified' do - let(:region) { provider.region } - let(:session_name) { "gitlab-eks-cluster-#{provider.cluster_id}-user-#{user.id}" } - let(:session_policy) { nil } - - it { is_expected.to eq assumed_role_credentials } - end - - context 'provider is not specifed' do - let(:provider) { nil } - let(:region) { provision_role.region } - let(:session_name) { "gitlab-eks-autofill-user-#{user.id}" } - let(:session_policy) { 'policy-document' } - - subject { described_class.new(provision_role, provider: provider).execute } - - before do - stub_file_read(Rails.root.join('vendor', 'aws', 'iam', 'eks_cluster_read_only_policy.json'), content: session_policy) - end - - it { is_expected.to eq assumed_role_credentials } - - context 'region is not specifed' do - let(:region) { Clusters::Providers::Aws::DEFAULT_REGION } - let(:provision_role) { create(:aws_role, user: user, region: nil) } - - it { is_expected.to eq assumed_role_credentials } - end - end - end - - context 'provision role is not configured' do - let(:provision_role) { nil } - - it 'raises an error' do - expect { subject }.to raise_error(described_class::MissingRoleError, 'AWS provisioning role not configured') - end - end - - context 'with an instance profile attached to an IAM role' do - let(:sts_client) { Aws::STS::Client.new(region: region, stub_responses: true) } - let(:provision_role) { create(:aws_role, user: user, region: 'custom-region') } - - before do - stub_application_setting(eks_access_key_id: nil) - stub_application_setting(eks_secret_access_key: nil) - - expect(Aws::STS::Client).to receive(:new) - .with(region: region) - .and_return(sts_client) - - expect(Aws::AssumeRoleCredentials).to receive(:new) - .with( - client: sts_client, - role_arn: provision_role.role_arn, - role_session_name: session_name, - external_id: provision_role.role_external_id, - policy: session_policy - ).and_call_original - end - - context 'provider is specified' do - let(:region) { provider.region } - let(:session_name) { "gitlab-eks-cluster-#{provider.cluster_id}-user-#{user.id}" } - let(:session_policy) { nil } - - it 'returns credentials', :aggregate_failures do - expect(subject.access_key_id).to be_present - expect(subject.secret_access_key).to be_present - expect(subject.session_token).to be_present - end - end - - context 'provider is not specifed' do - let(:provider) { nil } - let(:region) { provision_role.region } - let(:session_name) { "gitlab-eks-autofill-user-#{user.id}" } - let(:session_policy) { 'policy-document' } - - before do - stub_file_read(Rails.root.join('vendor', 'aws', 'iam', 'eks_cluster_read_only_policy.json'), content: session_policy) - end - - subject { described_class.new(provision_role, provider: provider).execute } - - it 'returns credentials', :aggregate_failures do - expect(subject.access_key_id).to be_present - expect(subject.secret_access_key).to be_present - expect(subject.session_token).to be_present - end - end - end - end -end diff --git a/spec/services/clusters/aws/finalize_creation_service_spec.rb b/spec/services/clusters/aws/finalize_creation_service_spec.rb deleted file mode 100644 index 6b0cb86eff0..00000000000 --- a/spec/services/clusters/aws/finalize_creation_service_spec.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Aws::FinalizeCreationService do - describe '#execute' do - let(:provider) { create(:cluster_provider_aws, :creating) } - let(:platform) { provider.cluster.platform_kubernetes } - - let(:create_service_account_service) { double(execute: true) } - let(:fetch_token_service) { double(execute: gitlab_token) } - let(:kube_client) { double(create_config_map: true) } - let(:cluster_stack) { double(outputs: [endpoint_output, cert_output, node_role_output]) } - let(:node_auth_config_map) { double } - - let(:endpoint_output) { double(output_key: 'ClusterEndpoint', output_value: api_url) } - let(:cert_output) { double(output_key: 'ClusterCertificate', output_value: Base64.encode64(ca_pem)) } - let(:node_role_output) { double(output_key: 'NodeInstanceRole', output_value: node_role) } - - let(:api_url) { 'https://kubernetes.example.com' } - let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) } - let(:gitlab_token) { 'gitlab-token' } - let(:iam_token) { 'iam-token' } - let(:node_role) { 'arn::aws::iam::123456789012:role/node-role' } - - subject { described_class.new.execute(provider) } - - before do - allow(Clusters::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:gitlab_creator) - .with(kube_client, rbac: true) - .and_return(create_service_account_service) - - allow(Clusters::Kubernetes::FetchKubernetesTokenService).to receive(:new) - .with( - kube_client, - Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, - Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE) - .and_return(fetch_token_service) - - allow(Gitlab::Kubernetes::KubeClient).to receive(:new) - .with( - api_url, - auth_options: { bearer_token: iam_token }, - ssl_options: { - verify_ssl: OpenSSL::SSL::VERIFY_PEER, - cert_store: instance_of(OpenSSL::X509::Store) - }, - http_proxy_uri: nil - ) - .and_return(kube_client) - - allow(provider.api_client).to receive(:describe_stacks) - .with(stack_name: provider.cluster.name) - .and_return(double(stacks: [cluster_stack])) - - allow(Kubeclient::AmazonEksCredentials).to receive(:token) - .with(provider.credentials, provider.cluster.name) - .and_return(iam_token) - - allow(Gitlab::Kubernetes::ConfigMaps::AwsNodeAuth).to receive(:new) - .with(node_role).and_return(double(generate: node_auth_config_map)) - end - - it 'configures the provider and platform' do - subject - - expect(provider).to be_created - expect(platform.api_url).to eq(api_url) - expect(platform.ca_pem).to eq(ca_pem) - expect(platform.token).to eq(gitlab_token) - expect(platform).to be_rbac - end - - it 'calls the create_service_account_service' do - expect(create_service_account_service).to receive(:execute).once - - subject - end - - it 'configures cluster node authentication' do - expect(kube_client).to receive(:create_config_map).with(node_auth_config_map).once - - subject - end - - describe 'error handling' do - shared_examples 'provision error' do |message| - it "sets the status to :errored with an appropriate error message" do - subject - - expect(provider).to be_errored - expect(provider.status_reason).to include(message) - end - end - - context 'failed to request stack details from AWS' do - before do - allow(provider.api_client).to receive(:describe_stacks) - .and_raise(Aws::CloudFormation::Errors::ServiceError.new(double, "Error message")) - end - - include_examples 'provision error', 'Failed to fetch CloudFormation stack' - end - - context 'failed to create auth config map' do - before do - allow(kube_client).to receive(:create_config_map) - .and_raise(Kubeclient::HttpError.new(500, 'Error', nil)) - end - - include_examples 'provision error', 'Failed to run Kubeclient' - end - - context 'failed to save records' do - before do - allow(provider.cluster).to receive(:save!) - .and_raise(ActiveRecord::RecordInvalid) - end - - include_examples 'provision error', 'Failed to configure EKS provider' - end - end - end -end diff --git a/spec/services/clusters/aws/provision_service_spec.rb b/spec/services/clusters/aws/provision_service_spec.rb deleted file mode 100644 index 5efac29ec1e..00000000000 --- a/spec/services/clusters/aws/provision_service_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Aws::ProvisionService do - describe '#execute' do - let(:provider) { create(:cluster_provider_aws) } - - let(:provision_role) { create(:aws_role, user: provider.created_by_user) } - let(:client) { instance_double(Aws::CloudFormation::Client, create_stack: true) } - let(:cloudformation_template) { double } - let(:credentials) do - instance_double( - Aws::Credentials, - access_key_id: 'key', - secret_access_key: 'secret', - session_token: 'token' - ) - end - - let(:parameters) do - [ - { parameter_key: 'ClusterName', parameter_value: provider.cluster.name }, - { parameter_key: 'ClusterRole', parameter_value: provider.role_arn }, - { parameter_key: 'KubernetesVersion', parameter_value: provider.kubernetes_version }, - { parameter_key: 'ClusterControlPlaneSecurityGroup', parameter_value: provider.security_group_id }, - { parameter_key: 'VpcId', parameter_value: provider.vpc_id }, - { parameter_key: 'Subnets', parameter_value: provider.subnet_ids.join(',') }, - { parameter_key: 'NodeAutoScalingGroupDesiredCapacity', parameter_value: provider.num_nodes.to_s }, - { parameter_key: 'NodeInstanceType', parameter_value: provider.instance_type }, - { parameter_key: 'KeyName', parameter_value: provider.key_name } - ] - end - - subject { described_class.new.execute(provider) } - - before do - allow(Clusters::Aws::FetchCredentialsService).to receive(:new) - .with(provision_role, provider: provider) - .and_return(double(execute: credentials)) - - allow(provider).to receive(:api_client) - .and_return(client) - - stub_file_read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'), content: cloudformation_template) - end - - it 'updates the provider status to :creating and configures the provider with credentials' do - subject - - expect(provider).to be_creating - expect(provider.access_key_id).to eq 'key' - expect(provider.secret_access_key).to eq 'secret' - expect(provider.session_token).to eq 'token' - end - - it 'creates a CloudFormation stack' do - expect(client).to receive(:create_stack).with( - stack_name: provider.cluster.name, - template_body: cloudformation_template, - parameters: parameters, - capabilities: ["CAPABILITY_IAM"] - ) - - subject - end - - it 'schedules a worker to monitor creation status' do - expect(WaitForClusterCreationWorker).to receive(:perform_in) - .with(Clusters::Aws::VerifyProvisionStatusService::INITIAL_INTERVAL, provider.cluster_id) - - subject - end - - describe 'error handling' do - shared_examples 'provision error' do |message| - it "sets the status to :errored with an appropriate error message" do - subject - - expect(provider).to be_errored - expect(provider.status_reason).to include(message) - end - end - - context 'invalid state transition' do - before do - allow(provider).to receive(:make_creating).and_return(false) - end - - include_examples 'provision error', 'Failed to update provider record' - end - - context 'AWS role is not configured' do - before do - allow(Clusters::Aws::FetchCredentialsService).to receive(:new) - .and_raise(Clusters::Aws::FetchCredentialsService::MissingRoleError) - end - - include_examples 'provision error', 'Amazon role is not configured' - end - - context 'AWS credentials are not configured' do - before do - allow(Clusters::Aws::FetchCredentialsService).to receive(:new) - .and_raise(Aws::Errors::MissingCredentialsError) - end - - include_examples 'provision error', 'Amazon credentials are not configured' - end - - context 'Authentication failure' do - before do - allow(Clusters::Aws::FetchCredentialsService).to receive(:new) - .and_raise(Aws::STS::Errors::ServiceError.new(double, 'Error message')) - end - - include_examples 'provision error', 'Amazon authentication failed' - end - - context 'CloudFormation failure' do - before do - allow(client).to receive(:create_stack) - .and_raise(Aws::CloudFormation::Errors::ServiceError.new(double, 'Error message')) - end - - include_examples 'provision error', 'Amazon CloudFormation request failed' - end - end - end -end diff --git a/spec/services/clusters/aws/verify_provision_status_service_spec.rb b/spec/services/clusters/aws/verify_provision_status_service_spec.rb deleted file mode 100644 index b9a58b97842..00000000000 --- a/spec/services/clusters/aws/verify_provision_status_service_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Aws::VerifyProvisionStatusService do - describe '#execute' do - let(:provider) { create(:cluster_provider_aws) } - - let(:stack) { double(stack_status: stack_status, creation_time: creation_time) } - let(:creation_time) { 1.minute.ago } - - subject { described_class.new.execute(provider) } - - before do - allow(provider.api_client).to receive(:describe_stacks) - .with(stack_name: provider.cluster.name) - .and_return(double(stacks: [stack])) - end - - shared_examples 'provision error' do |message| - it "sets the status to :errored with an appropriate error message" do - subject - - expect(provider).to be_errored - expect(provider.status_reason).to include(message) - end - end - - context 'stack creation is still in progress' do - let(:stack_status) { 'CREATE_IN_PROGRESS' } - let(:verify_service) { double(execute: true) } - - it 'schedules a worker to check again later' do - expect(WaitForClusterCreationWorker).to receive(:perform_in) - .with(described_class::POLL_INTERVAL, provider.cluster_id) - - subject - end - - context 'stack creation is taking too long' do - let(:creation_time) { 1.hour.ago } - - include_examples 'provision error', 'Kubernetes cluster creation time exceeds timeout' - end - end - - context 'stack creation is complete' do - let(:stack_status) { 'CREATE_COMPLETE' } - let(:finalize_service) { double(execute: true) } - - it 'finalizes creation' do - expect(Clusters::Aws::FinalizeCreationService).to receive(:new).and_return(finalize_service) - expect(finalize_service).to receive(:execute).with(provider).once - - subject - end - end - - context 'stack creation failed' do - let(:stack_status) { 'CREATE_FAILED' } - - include_examples 'provision error', 'Unexpected status' - end - - context 'error communicating with CloudFormation API' do - let(:stack_status) { 'CREATE_IN_PROGRESS' } - - before do - allow(provider.api_client).to receive(:describe_stacks) - .and_raise(Aws::CloudFormation::Errors::ServiceError.new(double, 'Error message')) - end - - include_examples 'provision error', 'Amazon CloudFormation request failed' - end - end -end diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 6e252bee7c0..95f10cdbd80 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -54,7 +54,6 @@ RSpec.describe Clusters::CreateService do let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } it 'creates another cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) expect { subject }.to change { Clusters::Cluster.count }.by(1) end end diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb deleted file mode 100644 index 990cc745382..00000000000 --- a/spec/services/clusters/gcp/fetch_operation_service_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Gcp::FetchOperationService do - include GoogleApi::CloudPlatformHelpers - - describe '#execute' do - let(:provider) { create(:cluster_provider_gcp, :creating) } - let(:gcp_project_id) { provider.gcp_project_id } - let(:zone) { provider.zone } - let(:operation_id) { provider.operation_id } - - shared_examples 'success' do - it 'yields' do - expect { |b| described_class.new.execute(provider, &b) } - .to yield_with_args - end - end - - shared_examples 'error' do - it 'sets an error to provider object' do - expect { |b| described_class.new.execute(provider, &b) } - .not_to yield_with_args - expect(provider.reload).to be_errored - end - end - - context 'when succeeded to fetch operation' do - before do - stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id) - end - - it_behaves_like 'success' - end - - context 'when Internal Server Error happened' do - before do - stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id) - end - - it_behaves_like 'error' - end - end -end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb deleted file mode 100644 index 9c553d0eec2..00000000000 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ /dev/null @@ -1,161 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Gcp::FinalizeCreationService, '#execute' do - include GoogleApi::CloudPlatformHelpers - include KubernetesHelpers - - let(:cluster) { create(:cluster, :project, :providing_by_gcp) } - let(:provider) { cluster.provider } - let(:platform) { cluster.platform } - let(:endpoint) { '111.111.111.111' } - let(:api_url) { 'https://' + endpoint } - let(:secret_name) { 'gitlab-token' } - let(:token) { 'sample-token' } - let(:namespace) { "#{cluster.project.path}-#{cluster.project.id}" } - - subject { described_class.new.execute(provider) } - - shared_examples 'success' do - it 'configures provider and kubernetes' do - subject - - expect(provider).to be_created - end - - it 'properly configures database models' do - subject - - cluster.reload - - expect(provider.endpoint).to eq(endpoint) - expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert).strip) - expect(platform.token).to eq(token) - end - end - - shared_examples 'error' do - it 'sets an error to provider object' do - subject - - expect(provider.reload).to be_errored - end - end - - shared_examples 'kubernetes information not successfully fetched' do - context 'when failed to fetch gke cluster info' do - before do - stub_cloud_platform_get_zone_cluster_error(provider.gcp_project_id, provider.zone, cluster.name) - end - - it_behaves_like 'error' - end - - context 'when token is empty' do - let(:token) { '' } - - it_behaves_like 'error' - end - - context 'when failed to fetch kubernetes token' do - before do - stub_kubeclient_get_secret_error(api_url, secret_name, namespace: 'default') - end - - it_behaves_like 'error' - end - - context 'when service account fails to create' do - before do - stub_kubeclient_create_service_account_error(api_url, namespace: 'default') - end - - it_behaves_like 'error' - end - end - - shared_context 'kubernetes information successfully fetched' do - before do - stub_cloud_platform_get_zone_cluster( - provider.gcp_project_id, provider.zone, cluster.name, { endpoint: endpoint } - ) - - stub_kubeclient_discover(api_url) - stub_kubeclient_get_namespace(api_url) - stub_kubeclient_create_namespace(api_url) - stub_kubeclient_get_service_account_error(api_url, 'gitlab') - stub_kubeclient_create_service_account(api_url) - stub_kubeclient_create_secret(api_url) - stub_kubeclient_put_secret(api_url, 'gitlab-token') - - stub_kubeclient_get_secret( - api_url, - metadata_name: secret_name, - token: Base64.encode64(token), - namespace: 'default' - ) - - stub_kubeclient_put_cluster_role_binding(api_url, 'gitlab-admin') - end - end - - context 'With a legacy ABAC cluster' do - before do - provider.legacy_abac = true - end - - include_context 'kubernetes information successfully fetched' - - it_behaves_like 'success' - - it 'uses ABAC authorization type' do - subject - cluster.reload - - expect(platform).to be_abac - expect(platform.authorization_type).to eq('abac') - end - - it_behaves_like 'kubernetes information not successfully fetched' - end - - context 'With an RBAC cluster' do - before do - provider.legacy_abac = false - end - - include_context 'kubernetes information successfully fetched' - - it_behaves_like 'success' - - it 'uses RBAC authorization type' do - subject - cluster.reload - - expect(platform).to be_rbac - expect(platform.authorization_type).to eq('rbac') - end - - it_behaves_like 'kubernetes information not successfully fetched' - end - - context 'With a Cloud Run cluster' do - before do - provider.cloud_run = true - end - - include_context 'kubernetes information successfully fetched' - - it_behaves_like 'success' - - it 'has knative pre-installed' do - subject - cluster.reload - - expect(cluster.application_knative).to be_present - expect(cluster.application_knative).to be_pre_installed - end - end -end diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb deleted file mode 100644 index c8b7f628e5b..00000000000 --- a/spec/services/clusters/gcp/provision_service_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Gcp::ProvisionService do - include GoogleApi::CloudPlatformHelpers - - describe '#execute' do - let(:provider) { create(:cluster_provider_gcp, :scheduled) } - let(:gcp_project_id) { provider.gcp_project_id } - let(:zone) { provider.zone } - - shared_examples 'success' do - it 'schedules a worker for status minitoring' do - expect(WaitForClusterCreationWorker).to receive(:perform_in) - - described_class.new.execute(provider) - - expect(provider.reload).to be_creating - end - end - - shared_examples 'error' do - it 'sets an error to provider object' do - described_class.new.execute(provider) - - expect(provider.reload).to be_errored - end - end - - context 'when succeeded to request provision' do - before do - stub_cloud_platform_create_cluster(gcp_project_id, zone) - end - - it_behaves_like 'success' - end - - context 'when operation status is unexpected' do - before do - stub_cloud_platform_create_cluster( - gcp_project_id, zone, - { - "status": 'unexpected' - }) - end - - it_behaves_like 'error' - end - - context 'when selfLink is unexpected' do - before do - stub_cloud_platform_create_cluster( - gcp_project_id, zone, - { - "selfLink": 'unexpected' - }) - end - - it_behaves_like 'error' - end - - context 'when Internal Server Error happened' do - before do - stub_cloud_platform_create_cluster_error(gcp_project_id, zone) - end - - it_behaves_like 'error' - end - end -end diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb deleted file mode 100644 index ffe4516c02b..00000000000 --- a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do - include GoogleApi::CloudPlatformHelpers - - describe '#execute' do - let(:provider) { create(:cluster_provider_gcp, :creating) } - let(:gcp_project_id) { provider.gcp_project_id } - let(:zone) { provider.zone } - let(:operation_id) { provider.operation_id } - - shared_examples 'continue_creation' do - it 'schedules a worker for status minitoring' do - expect(WaitForClusterCreationWorker).to receive(:perform_in) - - described_class.new.execute(provider) - end - end - - shared_examples 'finalize_creation' do - it 'schedules a worker for status minitoring' do - expect_next_instance_of(Clusters::Gcp::FinalizeCreationService) do |instance| - expect(instance).to receive(:execute) - end - - described_class.new.execute(provider) - end - end - - shared_examples 'error' do - it 'sets an error to provider object' do - described_class.new.execute(provider) - - expect(provider.reload).to be_errored - end - end - - context 'when operation status is RUNNING' do - before do - stub_cloud_platform_get_zone_operation( - gcp_project_id, zone, operation_id, - { - "status": 'RUNNING', - "startTime": 1.minute.ago.strftime("%FT%TZ") - }) - end - - it_behaves_like 'continue_creation' - - context 'when cluster creation time exceeds timeout' do - before do - stub_cloud_platform_get_zone_operation( - gcp_project_id, zone, operation_id, - { - "status": 'RUNNING', - "startTime": 30.minutes.ago.strftime("%FT%TZ") - }) - end - - it_behaves_like 'error' - end - end - - context 'when operation status is PENDING' do - before do - stub_cloud_platform_get_zone_operation( - gcp_project_id, zone, operation_id, - { - "status": 'PENDING', - "startTime": 1.minute.ago.strftime("%FT%TZ") - }) - end - - it_behaves_like 'continue_creation' - end - - context 'when operation status is DONE' do - before do - stub_cloud_platform_get_zone_operation( - gcp_project_id, zone, operation_id, - { - "status": 'DONE' - }) - end - - it_behaves_like 'finalize_creation' - end - - context 'when operation status is unexpected' do - before do - stub_cloud_platform_get_zone_operation( - gcp_project_id, zone, operation_id, - { - "status": 'unexpected' - }) - end - - it_behaves_like 'error' - end - - context 'when failed to get operation status' do - before do - stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id) - end - - it_behaves_like 'error' - end - end -end diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb index f8a58a828ce..80fa7c58515 100644 --- a/spec/support/services/clusters/create_service_shared.rb +++ b/spec/support/services/clusters/create_service_shared.rb @@ -37,9 +37,7 @@ RSpec.shared_context 'invalid cluster create params' do end RSpec.shared_examples 'create cluster service success' do - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) - + it 'creates a cluster object' do expect { subject } .to change { Clusters::Cluster.count }.by(1) .and change { Clusters::Providers::Gcp.count }.by(1) @@ -60,7 +58,6 @@ end RSpec.shared_examples 'create cluster service error' do it 'returns an error' do - expect(ClusterProvisionWorker).not_to receive(:perform_async) expect { subject }.to change { Clusters::Cluster.count }.by(0) expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present end 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 02915206cc5..446bc4cd92f 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 @@ -42,6 +42,10 @@ RSpec.shared_examples 'issuables list meta-data' do |issuable_type, action = nil let(:result_issuable) { issuables.first } let(:search) { result_issuable.title } + before do + stub_application_setting(search_rate_limit: 0, search_rate_limit_unauthenticated: 0) + end + # .simple_sorts is the same across all Sortable classes sorts = ::Issue.simple_sorts.keys + %w[popularity priority label_priority] sorts.each do |sort| diff --git a/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb index 20edca1ee9f..b34038ca0e4 100644 --- a/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb +++ b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb @@ -5,7 +5,9 @@ # - current_user # - error_message # optional -RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:| +RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: false| + let(:error_message) { _('This endpoint has been requested too many times. Try again later.') } + context 'when rate limiter enabled', :freeze_time, :clean_gitlab_redis_rate_limiting do let(:expected_logger_attributes) do { @@ -25,8 +27,6 @@ RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:| end end - let(:error_message) { _('This endpoint has been requested too many times. Try again later.') } - before do allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(rate_limit_key).and_return(1) end @@ -37,12 +37,16 @@ RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:| request request - expect(response).to have_gitlab_http_status(:too_many_requests) + if graphql + expect_graphql_errors_to_include(error_message) + else + expect(response).to have_gitlab_http_status(:too_many_requests) - if example.metadata[:type] == :controller - expect(response.body).to eq(error_message) - else # it is API spec - expect(response.body).to eq({ message: { error: error_message } }.to_json) + if response.content_type == 'application/json' # it is API spec + expect(response.body).to eq({ message: { error: error_message } }.to_json) + else + expect(response.body).to eq(error_message) + end end end end @@ -57,7 +61,11 @@ RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:| request - expect(response).not_to have_gitlab_http_status(:too_many_requests) + if graphql + expect_graphql_errors_to_be_empty + else + expect(response).not_to have_gitlab_http_status(:too_many_requests) + end end end end diff --git a/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb index 9f67bd69560..fcde3b65b4f 100644 --- a/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb @@ -34,3 +34,35 @@ RSpec.shared_examples 'issuable anonymous search' do end end end + +RSpec.shared_examples 'issuable API rate-limited search' do + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do + let(:current_user) { user } + + def request + get api(url, current_user), params: { scope: 'all', search: issuable.title } + end + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated do + def request + get api(url), params: { scope: 'all', search: issuable.title } + end + end + + context 'when rate_limit_issuable_searches is disabled', :freeze_time, :clean_gitlab_redis_rate_limiting do + before do + stub_feature_flags(rate_limit_issuable_searches: false) + + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold) + .with(:search_rate_limit_unauthenticated).and_return(1) + end + + it 'does not enforce the rate limit' do + get api(url), params: { scope: 'all', search: issuable.title } + get api(url), params: { scope: 'all', search: issuable.title } + + expect(response).to have_gitlab_http_status(:ok) + end + end +end diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb deleted file mode 100644 index 2d6ca4ab7e3..00000000000 --- a/spec/workers/cluster_provision_worker_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ClusterProvisionWorker do - describe '#perform' do - context 'when provider type is gcp' do - let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) } - let(:provider) { create(:cluster_provider_gcp, :scheduled) } - - it 'provision a cluster' do - expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute).with(provider) - - described_class.new.perform(cluster.id) - end - end - - context 'when provider type is aws' do - let(:cluster) { create(:cluster, provider_type: :aws, provider_aws: provider) } - let(:provider) { create(:cluster_provider_aws, :scheduled) } - - it 'provision a cluster' do - expect_any_instance_of(Clusters::Aws::ProvisionService).to receive(:execute).with(provider) - - described_class.new.perform(cluster.id) - end - end - - context 'when provider type is user' do - let(:cluster) { create(:cluster, :provided_by_user) } - - it 'does not provision a cluster' do - expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute) - - described_class.new.perform(cluster.id) - end - end - - context 'when cluster does not exist' do - it 'does not provision a cluster' do - expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute) - - described_class.new.perform(123) - end - end - end -end diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb deleted file mode 100644 index 9079dff1afe..00000000000 --- a/spec/workers/wait_for_cluster_creation_worker_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe WaitForClusterCreationWorker do - describe '#perform' do - context 'when provider type is gcp' do - let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) } - let(:provider) { create(:cluster_provider_gcp, :creating) } - - it 'provisions a cluster' do - expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute).with(provider) - - described_class.new.perform(cluster.id) - end - end - - context 'when provider type is aws' do - let(:cluster) { create(:cluster, provider_type: :aws, provider_aws: provider) } - let(:provider) { create(:cluster_provider_aws, :creating) } - - it 'provisions a cluster' do - expect_any_instance_of(Clusters::Aws::VerifyProvisionStatusService).to receive(:execute).with(provider) - - described_class.new.perform(cluster.id) - end - end - - context 'when provider type is user' do - let(:cluster) { create(:cluster, provider_type: :user) } - - it 'does not provision a cluster' do - expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute) - - described_class.new.perform(cluster.id) - end - end - - context 'when cluster does not exist' do - it 'does not provision a cluster' do - expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute) - - described_class.new.perform(123) - end - end - end -end diff --git a/vendor/aws/cloudformation/eks_cluster.yaml b/vendor/aws/cloudformation/eks_cluster.yaml deleted file mode 100644 index 8d93734fd46..00000000000 --- a/vendor/aws/cloudformation/eks_cluster.yaml +++ /dev/null @@ -1,342 +0,0 @@ ---- -AWSTemplateFormatVersion: "2010-09-09" -Description: GitLab EKS Cluster - -Parameters: - - KubernetesVersion: - Description: The Kubernetes version to install - Type: String - Default: "1.20" - AllowedValues: - - "1.16" - - "1.17" - - "1.18" - - "1.19" - - "1.20" - - KeyName: - Description: The EC2 Key Pair to allow SSH access to the node instances - Type: AWS::EC2::KeyPair::KeyName - - NodeImageIdSSMParam: - Type: "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>" - Default: /aws/service/eks/optimized-ami/1.17/amazon-linux-2/recommended/image_id - Description: AWS Systems Manager Parameter Store parameter of the AMI ID for the worker node instances. - - NodeInstanceType: - Description: EC2 instance type for the node instances - Type: String - Default: t3.medium - ConstraintDescription: Must be a valid EC2 instance type - AllowedValues: - - t2.small - - t2.medium - - t2.large - - t2.xlarge - - t2.2xlarge - - t3.nano - - t3.micro - - t3.small - - t3.medium - - t3.large - - t3.xlarge - - t3.2xlarge - - m3.medium - - m3.large - - m3.xlarge - - m3.2xlarge - - m4.large - - m4.xlarge - - m4.2xlarge - - m4.4xlarge - - m4.10xlarge - - m5.large - - m5.xlarge - - m5.2xlarge - - m5.4xlarge - - m5.12xlarge - - m5.24xlarge - - c4.large - - c4.xlarge - - c4.2xlarge - - c4.4xlarge - - c4.8xlarge - - c5.large - - c5.xlarge - - c5.2xlarge - - c5.4xlarge - - c5.9xlarge - - c5.18xlarge - - i3.large - - i3.xlarge - - i3.2xlarge - - i3.4xlarge - - i3.8xlarge - - i3.16xlarge - - r3.xlarge - - r3.2xlarge - - r3.4xlarge - - r3.8xlarge - - r4.large - - r4.xlarge - - r4.2xlarge - - r4.4xlarge - - r4.8xlarge - - r4.16xlarge - - x1.16xlarge - - x1.32xlarge - - p2.xlarge - - p2.8xlarge - - p2.16xlarge - - p3.2xlarge - - p3.8xlarge - - p3.16xlarge - - p3dn.24xlarge - - r5.large - - r5.xlarge - - r5.2xlarge - - r5.4xlarge - - r5.12xlarge - - r5.24xlarge - - r5d.large - - r5d.xlarge - - r5d.2xlarge - - r5d.4xlarge - - r5d.12xlarge - - r5d.24xlarge - - z1d.large - - z1d.xlarge - - z1d.2xlarge - - z1d.3xlarge - - z1d.6xlarge - - z1d.12xlarge - - NodeAutoScalingGroupDesiredCapacity: - Description: Desired capacity of Node Group ASG. - Type: Number - Default: 3 - - NodeVolumeSize: - Description: Node volume size - Type: Number - Default: 20 - - ClusterName: - Description: Unique name for your Amazon EKS cluster. - Type: String - - ClusterRole: - Description: The IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. - Type: String - - ClusterControlPlaneSecurityGroup: - Description: The security groups to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets. - Type: AWS::EC2::SecurityGroup::Id - - VpcId: - Description: The VPC to use for your EKS Cluster resources. - Type: AWS::EC2::VPC::Id - - Subnets: - Description: The subnets in your VPC where your worker nodes will run. - Type: List<AWS::EC2::Subnet::Id> - -Metadata: - - AWS::CloudFormation::Interface: - ParameterGroups: - - Label: - default: EKS Cluster - Parameters: - - ClusterName - - ClusterRole - - KubernetesVersion - - ClusterControlPlaneSecurityGroup - - Label: - default: Worker Node Configuration - Parameters: - - NodeAutoScalingGroupDesiredCapacity - - NodeInstanceType - - NodeImageIdSSMParam - - NodeVolumeSize - - KeyName - - Label: - default: Worker Network Configuration - Parameters: - - VpcId - - Subnets - -Resources: - - Cluster: - Type: AWS::EKS::Cluster - Properties: - Name: !Sub ${ClusterName} - Version: !Sub ${KubernetesVersion} - RoleArn: !Sub ${ClusterRole} - ResourcesVpcConfig: - SecurityGroupIds: - - !Ref ClusterControlPlaneSecurityGroup - SubnetIds: !Ref Subnets - - NodeInstanceProfile: - Type: AWS::IAM::InstanceProfile - Properties: - Path: "/" - Roles: - - !Ref NodeInstanceRole - - NodeInstanceRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - Path: "/" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy - - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy - - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly - - NodeSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: Security group for all nodes in the cluster - VpcId: !Ref VpcId - Tags: - - Key: !Sub kubernetes.io/cluster/${ClusterName} - Value: owned - - NodeSecurityGroupIngress: - Type: AWS::EC2::SecurityGroupIngress - DependsOn: NodeSecurityGroup - Properties: - Description: Allow nodes to communicate with each other - GroupId: !Ref NodeSecurityGroup - SourceSecurityGroupId: !Ref NodeSecurityGroup - IpProtocol: -1 - FromPort: 0 - ToPort: 65535 - - NodeSecurityGroupFromControlPlaneIngress: - Type: AWS::EC2::SecurityGroupIngress - DependsOn: NodeSecurityGroup - Properties: - Description: Allow worker Kubelets and pods to receive communication from the cluster control plane - GroupId: !Ref NodeSecurityGroup - SourceSecurityGroupId: !Ref ClusterControlPlaneSecurityGroup - IpProtocol: tcp - FromPort: 1025 - ToPort: 65535 - - ControlPlaneEgressToNodeSecurityGroup: - Type: AWS::EC2::SecurityGroupEgress - DependsOn: NodeSecurityGroup - Properties: - Description: Allow the cluster control plane to communicate with worker Kubelet and pods - GroupId: !Ref ClusterControlPlaneSecurityGroup - DestinationSecurityGroupId: !Ref NodeSecurityGroup - IpProtocol: tcp - FromPort: 1025 - ToPort: 65535 - - NodeSecurityGroupFromControlPlaneOn443Ingress: - Type: AWS::EC2::SecurityGroupIngress - DependsOn: NodeSecurityGroup - Properties: - Description: Allow pods running extension API servers on port 443 to receive communication from cluster control plane - GroupId: !Ref NodeSecurityGroup - SourceSecurityGroupId: !Ref ClusterControlPlaneSecurityGroup - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - - ControlPlaneEgressToNodeSecurityGroupOn443: - Type: AWS::EC2::SecurityGroupEgress - DependsOn: NodeSecurityGroup - Properties: - Description: Allow the cluster control plane to communicate with pods running extension API servers on port 443 - GroupId: !Ref ClusterControlPlaneSecurityGroup - DestinationSecurityGroupId: !Ref NodeSecurityGroup - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - - ClusterControlPlaneSecurityGroupIngress: - Type: AWS::EC2::SecurityGroupIngress - DependsOn: NodeSecurityGroup - Properties: - Description: Allow pods to communicate with the cluster API Server - GroupId: !Ref ClusterControlPlaneSecurityGroup - SourceSecurityGroupId: !Ref NodeSecurityGroup - IpProtocol: tcp - ToPort: 443 - FromPort: 443 - - NodeGroup: - Type: AWS::AutoScaling::AutoScalingGroup - DependsOn: Cluster - Properties: - DesiredCapacity: !Ref NodeAutoScalingGroupDesiredCapacity - LaunchConfigurationName: !Ref NodeLaunchConfig - MinSize: !Ref NodeAutoScalingGroupDesiredCapacity - MaxSize: !Ref NodeAutoScalingGroupDesiredCapacity - VPCZoneIdentifier: !Ref Subnets - Tags: - - Key: Name - Value: !Sub ${ClusterName}-node - PropagateAtLaunch: true - - Key: !Sub kubernetes.io/cluster/${ClusterName} - Value: owned - PropagateAtLaunch: true - UpdatePolicy: - AutoScalingRollingUpdate: - MaxBatchSize: 1 - MinInstancesInService: !Ref NodeAutoScalingGroupDesiredCapacity - PauseTime: PT5M - - NodeLaunchConfig: - Type: AWS::AutoScaling::LaunchConfiguration - Properties: - AssociatePublicIpAddress: true - IamInstanceProfile: !Ref NodeInstanceProfile - ImageId: !Ref NodeImageIdSSMParam - InstanceType: !Ref NodeInstanceType - KeyName: !Ref KeyName - SecurityGroups: - - !Ref NodeSecurityGroup - BlockDeviceMappings: - - DeviceName: /dev/xvda - Ebs: - VolumeSize: !Ref NodeVolumeSize - VolumeType: gp2 - DeleteOnTermination: true - UserData: - Fn::Base64: - !Sub | - #!/bin/bash - set -o xtrace - /etc/eks/bootstrap.sh "${ClusterName}" - /opt/aws/bin/cfn-signal --exit-code $? \ - --stack ${AWS::StackName} \ - --resource NodeGroup \ - --region ${AWS::Region} - -Outputs: - - NodeInstanceRole: - Description: The node instance role - Value: !GetAtt NodeInstanceRole.Arn - - ClusterCertificate: - Description: The cluster certificate - Value: !GetAtt Cluster.CertificateAuthorityData - - ClusterEndpoint: - Description: The cluster endpoint - Value: !GetAtt Cluster.Endpoint diff --git a/vendor/aws/iam/eks_cluster_read_only_policy.json b/vendor/aws/iam/eks_cluster_read_only_policy.json deleted file mode 100644 index 425b9a3eff9..00000000000 --- a/vendor/aws/iam/eks_cluster_read_only_policy.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "iam:ListRoles", - "ec2:DescribeKeyPairs", - "ec2:DescribeRegions", - "ec2:DescribeSecurityGroups", - "ec2:DescribeSubnets", - "ec2:DescribeVpcs" - ], - "Resource": "*" - } - ] -} |