diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-17 03:09:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-17 03:09:37 +0300 |
commit | d5cf5cf4f77eec07a04604b1a0298452029df16f (patch) | |
tree | 7fafba2450f0cc0160fbacfbd94a0b11ab47dc12 | |
parent | 831b6108d2aa46aca9bdce39a9bda33718d61fa7 (diff) |
Add latest changes from gitlab-org/gitlab@master
128 files changed, 3221 insertions, 1022 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/alert_management/components/alert_sidebar.vue index 8957ee410a3..64e4089c85a 100644 --- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue +++ b/app/assets/javascripts/alert_management/components/alert_sidebar.vue @@ -51,9 +51,18 @@ export default { <div class="issuable-sidebar js-issuable-update"> <sidebar-header :sidebar-collapsed="sidebarStatus" + :project-path="projectPath" + :alert="alert" @toggle-sidebar="$emit('toggle-sidebar')" + @alert-error="$emit('alert-error', $event)" + /> + <sidebar-todo + v-if="sidebarStatus" + :project-path="projectPath" + :alert="alert" + :sidebar-collapsed="sidebarStatus" + @alert-error="$emit('alert-error', $event)" /> - <sidebar-todo v-if="sidebarStatus" :sidebar-collapsed="sidebarStatus" /> <sidebar-status :project-path="projectPath" :alert="alert" diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue index 7a6bb79c010..cb32a5ffd4f 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue @@ -167,7 +167,7 @@ export default { if (errors[0]) { this.$emit( - 'alert-sidebar-error', + 'alert-error', `${this.$options.i18n.UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR} ${errors[0]}.`, ); } diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue index 047793d8cee..fd40b5d9f65 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue @@ -8,6 +8,14 @@ export default { SidebarTodo, }, props: { + alert: { + type: Object, + required: true, + }, + projectPath: { + type: String, + required: true, + }, sidebarCollapsed: { type: Boolean, required: true, @@ -17,18 +25,17 @@ export default { </script> <template> - <div class="block d-flex justify-content-between"> + <div class="block gl-display-flex gl-justify-content-space-between"> <span class="issuable-header-text hide-collapsed"> - {{ __('Quick actions') }} + {{ __('To Do') }} </span> - <toggle-sidebar - :collapsed="sidebarCollapsed" - css-classes="ml-auto" - @toggle="$emit('toggle-sidebar')" + <sidebar-todo + v-if="!sidebarCollapsed" + :project-path="projectPath" + :alert="alert" + :sidebar-collapsed="sidebarCollapsed" + @alert-error="$emit('alert-error', $event)" /> - <!-- TODO: Implement after or as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/215946 --> - <template v-if="false"> - <sidebar-todo v-if="!sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" /> - </template> + <toggle-sidebar :collapsed="sidebarCollapsed" @toggle="$emit('toggle-sidebar')" /> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue index 87090165f82..7d3135ad50d 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue @@ -1,29 +1,123 @@ <script> +import { s__ } from '~/locale'; import Todo from '~/sidebar/components/todo_toggle/todo.vue'; +import axios from '~/lib/utils/axios_utils'; +import createAlertTodo from '../../graphql/mutations/alert_todo_create.graphql'; export default { + i18n: { + UPDATE_ALERT_TODO_ERROR: s__( + 'AlertManagement|There was an error while updating the To Do of the alert.', + ), + }, components: { Todo, }, props: { + alert: { + type: Object, + required: true, + }, + projectPath: { + type: String, + required: true, + }, sidebarCollapsed: { type: Boolean, required: true, }, }, + data() { + return { + isUpdating: false, + isTodo: false, + todo: '', + }; + }, + computed: { + alertID() { + return parseInt(this.alert.iid, 10); + }, + }, + methods: { + updateToDoCount(add) { + const oldCount = parseInt(document.querySelector('.todos-count').innerText, 10); + const count = add ? oldCount + 1 : oldCount - 1; + const headerTodoEvent = new CustomEvent('todo:toggle', { + detail: { + count, + }, + }); + + return document.dispatchEvent(headerTodoEvent); + }, + toggleTodo() { + if (this.todo) { + return this.markAsDone(); + } + + this.isUpdating = true; + return this.$apollo + .mutate({ + mutation: createAlertTodo, + variables: { + iid: this.alert.iid, + projectPath: this.projectPath, + }, + }) + .then(({ data: { alertTodoCreate: { todo = {}, errors = [] } } = {} } = {}) => { + if (errors[0]) { + return this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${errors[0]}.`, + ); + } + + this.todo = todo.id; + return this.updateToDoCount(true); + }) + .catch(() => { + this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${s__( + 'AlertManagement|Please try again.', + )}`, + ); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + markAsDone() { + this.isUpdating = true; + + return axios + .delete(`/dashboard/todos/${this.todo.split('/').pop()}`) + .then(() => { + this.todo = ''; + return this.updateToDoCount(false); + }) + .catch(() => { + this.$emit('alert-error', this.$options.i18n.UPDATE_ALERT_TODO_ERROR); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + }, }; </script> -<!-- TODO: Implement after or as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/215946 --> <template> - <div v-if="false" :class="{ 'block todo': sidebarCollapsed }"> + <div :class="{ 'block todo': sidebarCollapsed, 'gl-ml-auto': !sidebarCollapsed }"> <todo + data-testid="alert-todo-button" :collapsed="sidebarCollapsed" - :issuable-id="1" - :is-todo="false" - :is-action-active="false" + :issuable-id="alertID" + :is-todo="todo !== ''" + :is-action-active="isUpdating" issuable-type="alert" - @toggleTodo="() => {}" + @toggleTodo="toggleTodo" /> </div> </template> diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql new file mode 100644 index 00000000000..cdf3d763302 --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql @@ -0,0 +1,11 @@ +mutation($projectPath: ID!, $iid: String!) { + alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) { + errors + alert { + iid + } + todo { + id + } + } +} diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index d151cecf5be..3f9163e924d 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -16,10 +16,11 @@ import Tracking from '~/tracking'; */ export default function initTodoToggle() { $(document).on('todo:toggle', (e, count) => { + const updatedCount = count || e?.detail?.count || 0; const $todoPendingCount = $('.todos-count'); - $todoPendingCount.text(highCountTrim(count)); - $todoPendingCount.toggleClass('hidden', count === 0); + $todoPendingCount.text(highCountTrim(updatedCount)); + $todoPendingCount.toggleClass('hidden', updatedCount === 0); }); } diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index eff125a1957..f4fc7decb60 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -5,6 +5,11 @@ module NotesActions include Gitlab::Utils::StrongMemoize extend ActiveSupport::Concern + # last_fetched_at is an integer number of microseconds, which is the same + # precision as PostgreSQL "timestamp" fields. It's important for them to have + # identical precision for accurate pagination + MICROSECOND = 1_000_000 + included do before_action :set_polling_interval_header, only: [:index] before_action :require_noteable!, only: [:index, :create] @@ -13,30 +18,20 @@ module NotesActions end def index - notes_json = { notes: [], last_fetched_at: Time.current.to_i } - - notes = notes_finder - .execute - .inc_relations_for_view - - if notes_filter != UserPreference::NOTES_FILTERS[:only_comments] - notes = - ResourceEvents::MergeIntoNotesService - .new(noteable, current_user, last_fetched_at: last_fetched_at) - .execute(notes) - end - + notes, meta = gather_notes notes = prepare_notes_for_rendering(notes) notes = notes.select { |n| n.readable_by?(current_user) } - - notes_json[:notes] = + notes = if use_note_serializer? note_serializer.represent(notes) else notes.map { |note| note_json(note) } end - render json: notes_json + # We know there's more data, so tell the frontend to poll again after 1ms + set_polling_interval_header(interval: 1) if meta[:more] + + render json: meta.merge(notes: notes) end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -101,6 +96,48 @@ module NotesActions private + # Lower bound (last_fetched_at as specified in the request) is already set in + # the finder. Here, we select between returning all notes since then, or a + # page's worth of notes. + def gather_notes + if Feature.enabled?(:paginated_notes, project) + gather_some_notes + else + gather_all_notes + end + end + + def gather_all_notes + now = Time.current + notes = merge_resource_events(notes_finder.execute.inc_relations_for_view) + + [notes, { last_fetched_at: (now.to_i * MICROSECOND) + now.usec }] + end + + def gather_some_notes + paginator = Gitlab::UpdatedNotesPaginator.new( + notes_finder.execute.inc_relations_for_view, + last_fetched_at: last_fetched_at + ) + + notes = paginator.notes + + # Fetch all the synthetic notes in the same time range as the real notes. + # Although we don't limit the number, their text is under our control so + # should be fairly cheap to process. + notes = merge_resource_events(notes, fetch_until: paginator.next_fetched_at) + + [notes, paginator.metadata] + end + + def merge_resource_events(notes, fetch_until: nil) + return notes if notes_filter == UserPreference::NOTES_FILTERS[:only_comments] + + ResourceEvents::MergeIntoNotesService + .new(noteable, current_user, last_fetched_at: last_fetched_at, fetch_until: fetch_until) + .execute(notes) + end + def note_html(note) render_to_string( "shared/notes/_note", @@ -229,8 +266,8 @@ module NotesActions params.require(:note).permit(:note, :position) end - def set_polling_interval_header - Gitlab::PollingInterval.set_header(response, interval: 6_000) + def set_polling_interval_header(interval: 6000) + Gitlab::PollingInterval.set_header(response, interval: interval) end def noteable @@ -242,7 +279,14 @@ module NotesActions end def last_fetched_at - request.headers['X-Last-Fetched-At'] + strong_memoize(:last_fetched_at) do + microseconds = request.headers['X-Last-Fetched-At'].to_i + + seconds = microseconds / MICROSECOND + frac = microseconds % MICROSECOND + + Time.zone.at(seconds, frac) + end end def notes_filter diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index afdea4f7c9d..bc05030f8af 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -30,7 +30,7 @@ class Import::BaseController < ApplicationController end def incompatible_repos - [] + raise NotImplementedError end def provider_name @@ -87,15 +87,6 @@ class Import::BaseController < ApplicationController end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord - def find_jobs(import_type) - current_user.created_projects - .with_import_state - .where(import_type: import_type) - .to_json(only: [:id], methods: [:import_status]) - end - # rubocop: enable CodeReuse/ActiveRecord - # deprecated: being replaced by app/services/import/base_service.rb def find_or_create_namespace(names, owner) names = params[:target_namespace].presence || names diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 4886aeb5e3f..0ffd9ef8bdd 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -22,23 +22,8 @@ class Import::BitbucketController < Import::BaseController redirect_to status_import_bitbucket_url end - # rubocop: disable CodeReuse/ActiveRecord def status - return super if Feature.enabled?(:new_import_ui) - - bitbucket_client = Bitbucket::Client.new(credentials) - repos = bitbucket_client.repos(filter: sanitized_filter_param) - @repos, @incompatible_repos = repos.partition { |repo| repo.valid? } - - @already_added_projects = find_already_added_projects('bitbucket') - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } - end - # rubocop: enable CodeReuse/ActiveRecord - - def jobs - render json: find_jobs('bitbucket') + super end def realtime_changes diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 35f812db942..bee78cb3283 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -52,23 +52,8 @@ class Import::BitbucketServerController < Import::BaseController redirect_to status_import_bitbucket_server_path end - # rubocop: disable CodeReuse/ActiveRecord def status - return super if Feature.enabled?(:new_import_ui) - - @collection = client.repos(page_offset: page_offset, limit: limit_per_page, filter: sanitized_filter_param) - @repos, @incompatible_repos = @collection.partition { |repo| repo.valid? } - - # Use the import URL to filter beyond what BaseService#find_already_added_projects - @already_added_projects = filter_added_projects('bitbucket_server', @repos.map(&:browse_url)) - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) } - end - # rubocop: enable CodeReuse/ActiveRecord - - def jobs - render json: find_jobs('bitbucket_server') + super end def realtime_changes diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 91779a5d6cc..a34bc9c953f 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -50,14 +50,7 @@ class Import::FogbugzController < Import::BaseController return redirect_to new_import_fogbugz_path end - return super if Feature.enabled?(:new_import_ui) - - @repos = client.repos - - @already_added_projects = find_already_added_projects('fogbugz') - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.reject! { |repo| already_added_projects_names.include? repo.name } + super end # rubocop: enable CodeReuse/ActiveRecord @@ -65,10 +58,6 @@ class Import::FogbugzController < Import::BaseController super end - def jobs - render json: find_jobs('fogbugz') - end - def create repo = client.repo(params[:repo_id]) fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] } @@ -96,6 +85,11 @@ class Import::FogbugzController < Import::BaseController end # rubocop: enable CodeReuse/ActiveRecord + override :incompatible_repos + def incompatible_repos + [] + end + override :provider_name def provider_name :fogbugz diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 42c23fb29a7..efeff8439e4 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -21,15 +21,17 @@ class Import::GiteaController < Import::GithubController super end - private + protected - def host_key - :"#{provider}_host_url" + override :provider_name + def provider_name + :gitea end - override :provider - def provider - :gitea + private + + def host_key + :"#{provider_name}_host_url" end override :provider_url diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 097edcd6075..ac6b8c06d66 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Import::GithubController < Import::BaseController + extend ::Gitlab::Utils::Override + include ImportHelper include ActionView::Helpers::SanitizeHelper @@ -34,18 +36,11 @@ class Import::GithubController < Import::BaseController # Improving in https://gitlab.com/gitlab-org/gitlab-foss/issues/55585 client_repos - respond_to do |format| - format.json do - render json: { imported_projects: serialized_imported_projects, - provider_repos: serialized_provider_repos, - namespaces: serialized_namespaces } - end - format.html - end + super end def create - result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider) + result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider_name) if result[:status] == :success render json: serialized_imported_projects(result[:project]) @@ -55,44 +50,51 @@ class Import::GithubController < Import::BaseController end def realtime_changes - Gitlab::PollingInterval.set_header(response, interval: 3_000) - - render json: already_added_projects.to_json(only: [:id], methods: [:import_status]) + super end - private + protected - def import_params - params.permit(permitted_import_params) - end + # rubocop: disable CodeReuse/ActiveRecord + override :importable_repos + def importable_repos + already_added_projects_names = already_added_projects.pluck(:import_source) - def permitted_import_params - [:repo_id, :new_name, :target_namespace] + client_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) } end + # rubocop: enable CodeReuse/ActiveRecord - def serialized_imported_projects(projects = already_added_projects) - ProjectSerializer.new.represent(projects, serializer: :import, provider_url: provider_url) + override :incompatible_repos + def incompatible_repos + [] end - def serialized_provider_repos - repos = client_repos.reject { |repo| already_added_project_names.include? repo.full_name } - Import::ProviderRepoSerializer.new(current_user: current_user).represent(repos, provider: provider, provider_url: provider_url) + override :provider_name + def provider_name + :github end - def serialized_namespaces - NamespaceSerializer.new.represent(namespaces) + override :provider_url + def provider_url + strong_memoize(:provider_url) do + provider = Gitlab::Auth::OAuth::Provider.config_for('github') + + provider&.dig('url').presence || 'https://github.com' + end end - def already_added_projects - @already_added_projects ||= filtered(find_already_added_projects(provider)) + private + + def import_params + params.permit(permitted_import_params) end - def already_added_project_names - @already_added_projects_names ||= already_added_projects.pluck(:import_source) # rubocop:disable CodeReuse/ActiveRecord + def permitted_import_params + [:repo_id, :new_name, :target_namespace] end - def namespaces - current_user.manageable_groups_with_routes + def serialized_imported_projects(projects = already_added_projects) + ProjectSerializer.new.represent(projects, serializer: :import, provider_url: provider_url) end def expire_etag_cache @@ -118,29 +120,29 @@ class Import::GithubController < Import::BaseController end def import_enabled? - __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend + __send__("#{provider_name}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend end def realtime_changes_path - public_send("realtime_changes_import_#{provider}_path", format: :json) # rubocop:disable GitlabSecurity/PublicSend + public_send("realtime_changes_import_#{provider_name}_path", format: :json) # rubocop:disable GitlabSecurity/PublicSend end def new_import_url - public_send("new_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend + public_send("new_import_#{provider_name}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def status_import_url - public_send("status_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend + public_send("status_import_#{provider_name}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def callback_import_url - public_send("users_import_#{provider}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend + public_send("users_import_#{provider_name}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def provider_unauthorized session[access_token_key] = nil redirect_to new_import_url, - alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account." + alert: "Access denied to your #{Gitlab::ImportSources.title(provider_name.to_s)} account." end def provider_rate_limit(exception) @@ -151,29 +153,16 @@ class Import::GithubController < Import::BaseController end def access_token_key - :"#{provider}_access_token" + :"#{provider_name}_access_token" end def access_params { github_access_token: session[access_token_key] } end - # The following methods are overridden in subclasses - def provider - :github - end - - def provider_url - strong_memoize(:provider_url) do - provider = Gitlab::Auth::OAuth::Provider.config_for('github') - - provider&.dig('url').presence || 'https://github.com' - end - end - # rubocop: disable CodeReuse/ActiveRecord def logged_in_with_provider? - current_user.identities.exists?(provider: provider) + current_user.identities.exists?(provider: provider_name) end # rubocop: enable CodeReuse/ActiveRecord @@ -202,12 +191,6 @@ class Import::GithubController < Import::BaseController def filter_attribute :name end - - def filtered(collection) - return collection unless sanitized_filter_param - - collection.select { |item| item[filter_attribute].include?(sanitized_filter_param) } - end end Import::GithubController.prepend_if_ee('EE::Import::GithubController') diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index a95a67e208c..cc68eb02741 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -16,21 +16,8 @@ class Import::GitlabController < Import::BaseController redirect_to status_import_gitlab_url end - # rubocop: disable CodeReuse/ActiveRecord def status - return super if Feature.enabled?(:new_import_ui) - - @repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS) - - @already_added_projects = find_already_added_projects('gitlab') - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] } - end - # rubocop: enable CodeReuse/ActiveRecord - - def jobs - render json: find_jobs('gitlab') + super end def create @@ -63,6 +50,11 @@ class Import::GitlabController < Import::BaseController end # rubocop: enable CodeReuse/ActiveRecord + override :incompatible_repos + def incompatible_repos + [] + end + override :provider_name def provider_name :gitlab diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 8e57014f66e..1a3f011d9eb 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -158,13 +158,16 @@ class NotesFinder end # Notes changed since last fetch - # Uses overlapping intervals to avoid worrying about race conditions def since_fetch_at(notes) return notes unless @params[:last_fetched_at] # Default to 0 to remain compatible with old clients - last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) - notes.updated_after(last_fetched_at - FETCH_OVERLAP) + last_fetched_at = @params.fetch(:last_fetched_at, Time.at(0)) + + # Use overlapping intervals to avoid worrying about race conditions + last_fetched_at -= FETCH_OVERLAP + + notes.updated_after(last_fetched_at) end def notes_filter? diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index e9136919a7e..3f2e813d381 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -46,7 +46,7 @@ class UserRecentEventsFinder SQL # Workaround for https://github.com/rails/rails/issues/24193 - ensure_design_visibility(Event.from([Arel.sql(sql)])) + Event.from([Arel.sql(sql)]) end # rubocop: enable CodeReuse/ActiveRecord @@ -59,11 +59,4 @@ class UserRecentEventsFinder def projects target_user.project_interactions.to_sql end - - # TODO: remove when the :design_activity_events feature flag is removed. - def ensure_design_visibility(events) - return events if Feature.enabled?(:design_activity_events) - - events.not_design - end end diff --git a/app/graphql/mutations/alert_management/alerts/todo/create.rb b/app/graphql/mutations/alert_management/alerts/todo/create.rb new file mode 100644 index 00000000000..3dba96e43f1 --- /dev/null +++ b/app/graphql/mutations/alert_management/alerts/todo/create.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module Alerts + module Todo + class Create < Base + graphql_name 'AlertTodoCreate' + + def resolve(args) + alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + result = ::AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute + + prepare_response(result) + end + + private + + def prepare_response(result) + { + alert: result.payload[:alert], + todo: result.payload[:todo], + errors: result.error? ? [result.message] : [] + } + end + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index 7fcca63db51..0de4b9409e4 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -18,6 +18,11 @@ module Mutations null: true, description: "The alert after mutation" + field :todo, + Types::TodoType, + null: true, + description: "The todo after mutation" + field :issue, Types::IssueType, null: true, diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 33e4afdafe7..49d51b626b2 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -10,6 +10,7 @@ module Types mount_mutation Mutations::AlertManagement::CreateAlertIssue mount_mutation Mutations::AlertManagement::UpdateAlertStatus mount_mutation Mutations::AlertManagement::Alerts::SetAssignees + mount_mutation Mutations::AlertManagement::Alerts::Todo::Create mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb index a377c3aafdc..b797722fef8 100644 --- a/app/graphql/types/todo_target_enum.rb +++ b/app/graphql/types/todo_target_enum.rb @@ -6,6 +6,7 @@ module Types value 'ISSUE', value: 'Issue', description: 'An Issue' value 'MERGEREQUEST', value: 'MergeRequest', description: 'A MergeRequest' value 'DESIGN', value: 'DesignManagement::Design', description: 'A Design' + value 'ALERT', value: 'AlertManagement::Alert', description: 'An Alert' end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d731a231f98..207230fd92e 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -69,8 +69,6 @@ module EventsHelper end def designs_visible? - return false unless Feature.enabled?(:design_activity_events) - if @project design_activity_enabled?(@project) elsif @group diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 76b1c2d234c..c709c2950d6 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -92,6 +92,13 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id, reason)) end + def merge_when_pipeline_succeeds_email(recipient_id, merge_request_id, mwps_set_by_user_id, reason = nil) + setup_merge_request_mail(merge_request_id, recipient_id) + + @mwps_set_by = ::User.find(mwps_set_by_user_id) + mail_answer_thread(@merge_request, merge_request_thread_options(mwps_set_by_user_id, recipient_id, reason)) + end + private def setup_merge_request_mail(merge_request_id, recipient_id, present: false) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index f3a4076e69c..c70ac1428cd 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -177,6 +177,10 @@ class NotifyPreview < ActionMailer::Preview Notify.service_desk_thank_you_email(issue.id).message end + def merge_when_pipeline_succeeds_email + Notify.merge_when_pipeline_succeeds_email(user.id, merge_request.id, user.id).message + end + private def project diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 78ca2c02174..9ec407a10a4 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -38,6 +38,10 @@ class ApplicationRecord < ActiveRecord::Base false end + def self.at_most(count) + limit(count) + end + def self.safe_find_or_create_by!(*args) safe_find_or_create_by(*args).tap do |record| record.validate! unless record.persisted? diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 444368d0ef3..7af78960e35 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -159,7 +159,16 @@ module Clusters if ca_pem.present? opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + + file = Tempfile.new('cluster_ca_pem_temp') + begin + file.write(ca_pem) + file.rewind + opts[:cert_store].add_file(file.path) + ensure + file.close + file.unlink # deletes the temp file + end end opts diff --git a/app/models/event.rb b/app/models/event.rb index 6cd091ca217..56d7742c51a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -83,9 +83,6 @@ class Event < ApplicationRecord scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') } scope :for_design, -> { where(target_type: 'DesignManagement::Design') } - # Needed to implement feature flag: can be removed when feature flag is removed - scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') } - scope :with_associations, -> do # We're using preload for "push_event_payload" as otherwise the association # is not always available (depending on the query being built). diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb index ce062abeaaf..4768506b8fa 100644 --- a/app/models/event_collection.rb +++ b/app/models/event_collection.rb @@ -33,23 +33,16 @@ class EventCollection project_events end - relation = apply_feature_flags(relation) relation = paginate_events(relation) relation.with_associations.to_a end def all_project_events - apply_feature_flags(Event.from_union([project_events]).recent) + Event.from_union([project_events]).recent end private - def apply_feature_flags(events) - events = events.not_design unless ::Feature.enabled?(:design_activity_events) - - events - end - def project_events relation_with_join_lateral('project_id', projects) end diff --git a/app/models/note.rb b/app/models/note.rb index d35e27ccc7c..2db7e4e406d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -123,6 +123,8 @@ class Note < ApplicationRecord scope :common, -> { where(noteable_type: ["", nil]) } scope :fresh, -> { order(created_at: :asc, id: :asc) } scope :updated_after, ->(time) { where('updated_at > ?', time) } + scope :with_updated_at, ->(time) { where(updated_at: time) } + scope :by_updated_at, -> { reorder(:updated_at, :id) } scope :inc_author_project, -> { includes(:project, :author) } scope :inc_author, -> { includes(:author) } scope :inc_relations_for_view, -> do diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb index 552b6585db7..95a2e7a26c4 100644 --- a/app/models/product_analytics_event.rb +++ b/app/models/product_analytics_event.rb @@ -8,6 +8,8 @@ class ProductAnalyticsEvent < ApplicationRecord belongs_to :project + validates :event_id, :project_id, :v_collector, :v_etl, presence: true + # There is no default Rails timestamps in the table. # collector_tstamp is a timestamp when a collector recorded an event. scope :order_by_time, -> { order(collector_tstamp: :desc) } diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb index 86e11c2d568..26dcda2630a 100644 --- a/app/models/resource_event.rb +++ b/app/models/resource_event.rb @@ -11,6 +11,7 @@ class ResourceEvent < ApplicationRecord belongs_to :user scope :created_after, ->(time) { where('created_at > ?', time) } + scope :created_on_or_before, ->(time) { where('created_at <= ?', time) } def discussion_id strong_memoize(:discussion_id) do diff --git a/app/services/alert_management/alerts/todo/create_service.rb b/app/services/alert_management/alerts/todo/create_service.rb new file mode 100644 index 00000000000..87af943fdc2 --- /dev/null +++ b/app/services/alert_management/alerts/todo/create_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module AlertManagement + module Alerts + module Todo + class CreateService + # @param alert [AlertManagement::Alert] + # @param current_user [User] + def initialize(alert, current_user) + @alert = alert + @current_user = current_user + end + + def execute + return error_no_permissions unless allowed? + + todos = TodoService.new.mark_todo(alert, current_user) + todo = todos&.first + + return error_existing_todo unless todo + + success(todo) + end + + private + + attr_reader :alert, :current_user + + def allowed? + current_user&.can?(:update_alert_management_alert, alert) + end + + def error(message) + ServiceResponse.error(payload: { alert: alert, todo: nil }, message: message) + end + + def success(todo) + ServiceResponse.success(payload: { alert: alert, todo: todo }) + end + + def error_no_permissions + error(_('You have insufficient permissions to create a Todo for this alert')) + end + + def error_existing_todo + error(_('You already have pending todo for this alert')) + end + end + end + end +end diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index c4109765a1c..5c63dc34cb1 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -11,7 +11,7 @@ module AutoMerge yield if block_given? end - # Notify the event that auto merge is enabled or merge param is updated + notify(merge_request) AutoMergeProcessWorker.perform_async(merge_request.id) strategy.to_sym @@ -62,6 +62,10 @@ module AutoMerge private + # Overridden in child classes + def notify(merge_request) + end + def strategy strong_memoize(:strategy) do self.class.name.demodulize.remove('Service').underscore diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index 9ae5bd1b5ec..7e0298432ac 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -34,5 +34,13 @@ module AutoMerge merge_request.actual_head_pipeline&.active? end end + + private + + def notify(merge_request) + return unless Feature.enabled?(:mwps_notification, project) + + notification_service.async.merge_when_pipeline_succeeds(merge_request, current_user) if merge_request.saved_change_to_auto_merge_enabled? + end end end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 017a4f16b4c..ad36fe70b3a 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -83,8 +83,6 @@ class EventCreateService end def save_designs(current_user, create: [], update: []) - return [] unless Feature.enabled?(:design_activity_events) - records = create.zip([:created].cycle) + update.zip([:updated].cycle) return [] if records.empty? @@ -92,7 +90,6 @@ class EventCreateService end def destroy_designs(designs, current_user) - return [] unless Feature.enabled?(:design_activity_events) return [] unless designs.present? create_record_events(designs.zip([:destroyed].cycle), current_user) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 87664af3c10..a4e935a8cf5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -582,6 +582,14 @@ class NotificationService end end + def merge_when_pipeline_succeeds(merge_request, current_user) + recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'merge_when_pipeline_succeeds') + + recipients.each do |recipient| + mailer.merge_when_pipeline_succeeds_email(recipient.user.id, merge_request.id, current_user.id).deliver_later + end + end + protected def new_resource_email(target, method) diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb index db8bf6e4b74..a2d78ec67c3 100644 --- a/app/services/resource_events/base_synthetic_notes_builder_service.rb +++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb @@ -23,11 +23,25 @@ module ResourceEvents private - def since_fetch_at(events) + def apply_common_filters(events) + events = apply_last_fetched_at(events) + events = apply_fetch_until(events) + + events + end + + def apply_last_fetched_at(events) return events unless params[:last_fetched_at].present? - last_fetched_at = Time.zone.at(params.fetch(:last_fetched_at).to_i) - events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP) + last_fetched_at = params[:last_fetched_at] - NotesFinder::FETCH_OVERLAP + + events.created_after(last_fetched_at) + end + + def apply_fetch_until(events) + return events unless params[:fetch_until].present? + + events.created_on_or_before(params[:fetch_until]) end def resource_parent diff --git a/app/services/resource_events/synthetic_label_notes_builder_service.rb b/app/services/resource_events/synthetic_label_notes_builder_service.rb index fd128101b49..5915ea938cf 100644 --- a/app/services/resource_events/synthetic_label_notes_builder_service.rb +++ b/app/services/resource_events/synthetic_label_notes_builder_service.rb @@ -19,7 +19,7 @@ module ResourceEvents return [] unless resource.respond_to?(:resource_label_events) events = resource.resource_label_events.includes(:label, user: :status) # rubocop: disable CodeReuse/ActiveRecord - events = since_fetch_at(events) + events = apply_common_filters(events) events.group_by { |event| event.discussion_id } end diff --git a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb index cc6383d7083..10acf94e22b 100644 --- a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb +++ b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb @@ -19,7 +19,7 @@ module ResourceEvents return [] unless resource.respond_to?(:resource_milestone_events) events = resource.resource_milestone_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord - since_fetch_at(events) + apply_common_filters(events) end end end diff --git a/app/services/resource_events/synthetic_state_notes_builder_service.rb b/app/services/resource_events/synthetic_state_notes_builder_service.rb index 763134d98d8..71d40200365 100644 --- a/app/services/resource_events/synthetic_state_notes_builder_service.rb +++ b/app/services/resource_events/synthetic_state_notes_builder_service.rb @@ -14,7 +14,7 @@ module ResourceEvents return [] unless resource.respond_to?(:resource_state_events) events = resource.resource_state_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord - since_fetch_at(events) + apply_common_filters(events) end end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index e6fb0d3c72e..ec15bdde8d7 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -162,9 +162,9 @@ class TodoService create_assignment_todo(alert, current_user, []) end - # When user marks an issue as todo - def mark_todo(issuable, current_user) - attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED) + # When user marks a target as todo + def mark_todo(target, current_user) + attributes = attributes_for_todo(target.project, target, current_user, Todo::MARKED) create_todos(current_user, attributes) end diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json index e7e56f4e770..1154a4c45b8 100644 --- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json +++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json @@ -145,11 +145,6 @@ "enabled" : true }, { - "name": "tslint", - "label": "TSLint", - "enabled" : true - }, - { "name": "secrets", "label": "Secrets", "enabled" : true diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index d405acef75c..9b54cbe577a 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -5,93 +5,4 @@ %i.fa.fa-bitbucket = _('Import projects from Bitbucket') -- if Feature.enabled?(:new_import_ui) - = render 'import/githubish_status', provider: 'bitbucket' -- else - - if @repos.any? - %p.light - = _('Select projects you want to import.') - %p - - if @incompatible_repos.any? - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all compatible projects') - = icon('spinner spin', class: 'loading-icon') - - else - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all projects') - = icon('spinner spin', class: 'loading-icon') - - .position-relative.ms-no-clear.d-flex.flex-fill.float-right.append-bottom-10 - = form_tag status_import_bitbucket_path, method: 'get' do - = text_field_tag :filter, @filter, class: 'form-control pr-5', placeholder: _('Filter projects'), size: 40, autofocus: true, 'aria-label': _('Search') - .position-absolute.position-top-0.d-flex.align-items-center.text-muted.position-right-0.h-100 - .border-left - %button{ class: 'btn btn-transparent btn-secondary', 'aria-label': _('Search Button'), type: 'submit' } - %i{ class: 'fa fa-search', 'aria-hidden': true } - - .table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From Bitbucket') - %th= _('To GitLab') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer' - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - %span - %i.fa.fa-check - = _('done') - - when 'started' - %i.fa.fa-spinner.fa-spin - = _('started') - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } - %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-prepend - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true - %span.input-group-prepend - .input-group-text / - = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: 'btn btn-import js-add-to-import' do - = _('Import') - = icon('spinner spin', class: 'loading-icon') - - @incompatible_repos.each do |repo| - %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } - %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' - %td.import-target - %td.import-actions-job-status - = label_tag _('Incompatible Project'), nil, class: 'label badge-danger' - - - if @incompatible_repos.any? - %p - = _("One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git.") - - link_to_git = link_to(_('Git'), 'https://www.atlassian.com/git/tutorials/migrating-overview') - - link_to_import_flow = link_to(_('import flow'), status_import_bitbucket_path) - = _("Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again.").html_safe % { link_to_git: link_to_git, link_to_import_flow: link_to_import_flow } - - .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } += render 'import/githubish_status', provider: 'bitbucket' diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index c3d89470796..a24a1c1fb05 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -5,94 +5,4 @@ %i.fa.fa-bitbucket-square = _('Import projects from Bitbucket Server') -- if Feature.enabled?(:new_import_ui) - = render 'import/githubish_status', provider: 'bitbucket_server', extra_data: { reconfigure_path: configure_import_bitbucket_server_path } -- else - - if @repos.any? - %p.light - = _('Select projects you want to import.') - .btn-group - - if @incompatible_repos.any? - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all compatible projects') - = icon('spinner spin', class: 'loading-icon') - - else - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all projects') - = icon('spinner spin', class: 'loading-icon') - - .btn-group - = link_to('Reconfigure', configure_import_bitbucket_server_path, class: 'btn btn-primary', method: :post) - - .input-btn-group.float-right - = form_tag status_import_bitbucket_server_path, :method => 'get' do - = text_field_tag :filter, sanitize(params[:filter]), class: 'form-control append-bottom-10', placeholder: _('Filter your projects by name'), size: 40, autoFocus: true - - .table-responsive.prepend-top-10 - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From Bitbucket Server') - %th= _('To GitLab') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = link_to project.import_source, project.import_source, target: '_blank', rel: 'noopener noreferrer' - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - = icon('check', text: 'Done') - - when 'started' - = icon('spin', text: 'started') - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{ data: { id: "#{repo.project_key}/#{repo.slug}" } } - %td - = sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel)) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-prepend - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :extra_group - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true - %span.input-group-prepend - .input-group-text / - = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, required: true - %td.import-actions.job-status - = button_tag class: 'btn btn-import js-add-to-import' do - Import - = icon('spinner spin', class: 'loading-icon') - - @incompatible_repos.each do |repo| - %tr{ id: "repo_#{repo.project_key}___#{repo.slug}" } - %td - = sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel)) - %td.import-target - %td.import-actions-job-status - = label_tag 'Incompatible Project', nil, class: 'label badge-danger' - - - if @incompatible_repos.any? - %p - One or more of your Bitbucket Server projects cannot be imported into GitLab - directly because they use Subversion or Mercurial for version control, - rather than Git. Please convert - = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' - and go through the - = link_to 'import flow', status_import_bitbucket_server_path - again. - - = paginate_without_count(@collection) - - .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_server_path}", import_path: "#{import_bitbucket_server_path}" } } += render 'import/githubish_status', provider: 'bitbucket_server', extra_data: { reconfigure_path: configure_import_bitbucket_server_path } diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index 75529487aa4..f201c0e83fe 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -4,63 +4,8 @@ %i.fa.fa-bug = _('Import projects from FogBugz') -- if Feature.enabled?(:new_import_ui) - %p.light - - link_to_customize = link_to('customize', new_user_map_import_fogbugz_path) - = _('Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab.').html_safe % { link_to_customize: link_to_customize } - %hr - = render 'import/githubish_status', provider: 'fogbugz', filterable: false -- else - - if @repos.any? - %p.light - = _('Select projects you want to import.') - %p.light - - link_to_customize = link_to('customize', new_user_map_import_fogbugz_path) - = _('Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab.').html_safe % { link_to_customize: link_to_customize } - %hr - %p - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all projects') - = icon("spinner spin", class: "loading-icon") - - .table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _("From FogBugz") - %th= _("To GitLab") - %th= _("Status") - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = project.import_source - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - %span - %i.fa.fa-check - = _("done") - - when 'started' - %i.fa.fa-spinner.fa-spin - = _("started") - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{ id: "repo_#{repo.id}" } - %td - = repo.name - %td.import-target - #{current_user.username}/#{repo.name} - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - = _("Import") - = icon("spinner spin", class: "loading-icon") - - .js-importer-status{ data: { jobs_import_path: "#{jobs_import_fogbugz_path}", import_path: "#{import_fogbugz_path}" } } +%p.light + - link_to_customize = link_to('customize', new_user_map_import_fogbugz_path) + = _('Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab.').html_safe % { link_to_customize: link_to_customize } +%hr += render 'import/githubish_status', provider: 'fogbugz', filterable: false diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 5238ba6c92b..5513849be3d 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -4,55 +4,4 @@ = sprite_icon('heart', size: 16, css_class: 'gl-vertical-align-middle') = _('Import projects from GitLab.com') -- if Feature.enabled?(:new_import_ui) - = render 'import/githubish_status', provider: 'gitlab', filterable: false -- else - %p.light - = _('Select projects you want to import.') - %hr - %p - = button_tag class: "btn btn-import btn-success js-import-all" do - = _('Import all projects') - = icon("spinner spin", class: "loading-icon") - - .table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From GitLab.com') - %th= _('To this GitLab instance') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - %span - %i.fa.fa-check - = _('done') - - when 'started' - %i.fa.fa-spinner.fa-spin - = _('started') - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{ id: "repo_#{repo["id"]}" } - %td - = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer' - %td.import-target - = import_project_target(repo['namespace']['path'], repo['name']) - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - = _('Import') - = icon("spinner spin", class: "loading-icon") - - .js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitlab_path}", import_path: "#{import_gitlab_path}" } } += render 'import/githubish_status', provider: 'gitlab', filterable: false diff --git a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml new file mode 100644 index 00000000000..54c4043f575 --- /dev/null +++ b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml @@ -0,0 +1,161 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" } + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" } + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" } + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + + ul.assignees-list { + list-style: none; + padding: 0px; + display: block; + margin-top: 0px; + } + ul.assignees-list li { + display: inline-block; + padding-right: 12px; + padding-top: 8px; + } + + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" } + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + %tr.success + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + %span= _('Merge request was scheduled to merge after pipeline succeeds') + %tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + + %tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" } + %tbody + %tr{ style: 'width:100%;' } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" } + %img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" } + %span{ style: "font-weight: 600;color:#333333;" }= _('Merge request') + %a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference + %span= _('was scheduled to merge after pipeline succeeds by') + %img.avatar{ height: "24", src: avatar_icon_for_user(@mwps_set_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" } + %a.muted{ href: user_url(@mwps_set_by), style: "color:#333333;text-decoration:none;" } + = @mwps_set_by.name + %tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + + %tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" }= _('Project') + -# haml-lint:disable NoPlainNodes + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" }= _('Branch') + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %span.muted{ style: "color:#333333;text-decoration:none;" } + = @merge_request.source_branch + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" }= _('Author') + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" } + = @merge_request.author.name + + - if @merge_request.assignees.any? + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } + = assignees_label(@merge_request, include_value: false) + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; margin: 0; padding: 14px 0 0px 5px; font-size: 15px; line-height: 1.4; color: #333333; font-weight: 400; width: 75%; border-top-style: solid; border-top-color: #ededed; border-top-width: 1px; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" } + %ul.assignees-list{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; padding-right: 5px; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" } + - @merge_request.assignees.each do |assignee| + %li + %img.avatar{ alt: "Avatar", height: "24", src: avatar_icon_for_user(assignee, 24, only_path: false), style: "border-radius: 12px; max-width: 100%; height: auto; -ms-interpolation-mode: bicubic; margin: -2px 0;", width: "24" } + %a.muted{ href: user_url(assignee), style: "color: #333333; text-decoration: none; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; vertical-align: top;" } + = assignee.name + + -# EE-specific start + = render 'layouts/mailer/additional_text' + -# EE-specific end + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" } + %div + - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;") + - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;") + = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } diff --git a/app/views/notify/merge_when_pipeline_succeeds_email.text.haml b/app/views/notify/merge_when_pipeline_succeeds_email.text.haml new file mode 100644 index 00000000000..fdc23a6af0f --- /dev/null +++ b/app/views/notify/merge_when_pipeline_succeeds_email.text.haml @@ -0,0 +1,8 @@ +Merge Request #{@merge_request.to_reference} was scheduled to merge after pipeline succeeds by #{sanitize_name(@mwps_set_by.name)} + +Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} + += merge_path_description(@merge_request, 'to') + +Author: #{sanitize_name(@merge_request.author_name)} += assignees_label(@merge_request) diff --git a/changelogs/unreleased/215946-add-gitlab-to-do-for-user-when-they-are-assigned-to-an-alert-2.yml b/changelogs/unreleased/215946-add-gitlab-to-do-for-user-when-they-are-assigned-to-an-alert-2.yml new file mode 100644 index 00000000000..76e3770c65a --- /dev/null +++ b/changelogs/unreleased/215946-add-gitlab-to-do-for-user-when-they-are-assigned-to-an-alert-2.yml @@ -0,0 +1,5 @@ +--- +title: Add ability for user to manually create a todo for an alert +merge_request: 34175 +author: +type: added diff --git a/changelogs/unreleased/217673-keyset-paginate-notes-backend-only.yml b/changelogs/unreleased/217673-keyset-paginate-notes-backend-only.yml new file mode 100644 index 00000000000..dac9f4cb57b --- /dev/null +++ b/changelogs/unreleased/217673-keyset-paginate-notes-backend-only.yml @@ -0,0 +1,5 @@ +--- +title: Paginate the notes incremental fetch endpoint +merge_request: 34628 +author: +type: performance diff --git a/changelogs/unreleased/218526_backstage_remove_gitlab_issue_tracker_service_records.yml b/changelogs/unreleased/218526_backstage_remove_gitlab_issue_tracker_service_records.yml new file mode 100644 index 00000000000..c774e3d5397 --- /dev/null +++ b/changelogs/unreleased/218526_backstage_remove_gitlab_issue_tracker_service_records.yml @@ -0,0 +1,5 @@ +--- +title: Clean up GitlabIssueTrackerService database records +merge_request: 35221 +author: +type: other diff --git a/changelogs/unreleased/31000-api-for-instance-level-kubernetes-clusters.yml b/changelogs/unreleased/31000-api-for-instance-level-kubernetes-clusters.yml new file mode 100644 index 00000000000..8fe88130c59 --- /dev/null +++ b/changelogs/unreleased/31000-api-for-instance-level-kubernetes-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Add API support for instance-level Kubernetes clusters +merge_request: 36001 +author: +type: added diff --git a/changelogs/unreleased/ajk-remove-design-events-ff.yml b/changelogs/unreleased/ajk-remove-design-events-ff.yml new file mode 100644 index 00000000000..15b0142f917 --- /dev/null +++ b/changelogs/unreleased/ajk-remove-design-events-ff.yml @@ -0,0 +1,5 @@ +--- +title: Enable design activity events by default +merge_request: 37107 +author: +type: added diff --git a/changelogs/unreleased/merge-tslint-with-eslint.yml b/changelogs/unreleased/merge-tslint-with-eslint.yml new file mode 100644 index 00000000000..d9f9d83339a --- /dev/null +++ b/changelogs/unreleased/merge-tslint-with-eslint.yml @@ -0,0 +1,5 @@ +--- +title: Merge tslint secure analyzer with eslint secure analyzer +merge_request: 36400 +author: +type: changed diff --git a/changelogs/unreleased/patch-109.yml b/changelogs/unreleased/patch-109.yml new file mode 100644 index 00000000000..4b716f004d6 --- /dev/null +++ b/changelogs/unreleased/patch-109.yml @@ -0,0 +1,5 @@ +--- +title: Fixed issue (#198424) that prevented k8s authentication with intermediate certificates +merge_request: 31254 +author: Abdelrahman Mohamed +type: fixed diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 51b49bec864..b0778633199 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -68,6 +68,15 @@ class Rack::Attack end end + # Product analytics feature is in experimental stage. + # At this point we want to limit amount of events registered + # per application (aid stands for application id). + throttle('throttle_product_analytics_collector', limit: 100, period: 60) do |req| + if req.product_analytics_collector_request? + req.params['aid'] + end + end + throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| if req.web_request? && Gitlab::Throttle.settings.throttle_authenticated_web_enabled @@ -128,6 +137,10 @@ class Rack::Attack path =~ %r{^/-/(health|liveness|readiness)} end + def product_analytics_collector_request? + path.start_with?('/-/collector/i') + end + def should_be_skipped? api_internal_request? || health_check_request? end diff --git a/config/routes.rb b/config/routes.rb index 03a86d47646..dd84bc859bb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ require 'sidekiq/web' require 'sidekiq/cron/web' +require 'product_analytics/collector_app' Rails.application.routes.draw do concern :access_requestable do @@ -176,6 +177,9 @@ Rails.application.routes.draw do # Used by third parties to verify CI_JOB_JWT, placeholder route # in case we decide to move away from doorkeeper-openid_connect get 'jwks' => 'doorkeeper/openid_connect/discovery#keys' + + # Product analytics collector + match '/collector/i', to: ProductAnalytics::CollectorApp.new, via: :all end # End of the /-/ scope. diff --git a/config/routes/import.rb b/config/routes/import.rb index cd8278f6fd0..1dc27d489f0 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -24,14 +24,12 @@ namespace :import do resource :gitlab, only: [:create], controller: :gitlab do get :status get :callback - get :jobs get :realtime_changes end resource :bitbucket, only: [:create], controller: :bitbucket do get :status get :callback - get :jobs get :realtime_changes end @@ -39,7 +37,6 @@ namespace :import do post :configure get :status get :callback - get :jobs get :realtime_changes end @@ -55,7 +52,6 @@ namespace :import do resource :fogbugz, only: [:create, :new], controller: :fogbugz do get :status post :callback - get :jobs get :realtime_changes get :new_user_map, path: :user_map diff --git a/db/post_migrate/20200623142159_remove_gitlab_issue_tracker_service_records.rb b/db/post_migrate/20200623142159_remove_gitlab_issue_tracker_service_records.rb new file mode 100644 index 00000000000..743499e7b76 --- /dev/null +++ b/db/post_migrate/20200623142159_remove_gitlab_issue_tracker_service_records.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class RemoveGitlabIssueTrackerServiceRecords < ActiveRecord::Migration[6.0] + DOWNTIME = false + BATCH_SIZE = 5000 + + disable_ddl_transaction! + + class Service < ActiveRecord::Base + include EachBatch + + self.table_name = 'services' + + def self.gitlab_issue_tracker_service + where(type: 'GitlabIssueTrackerService') + end + end + + def up + Service.each_batch(of: BATCH_SIZE) do |services| + services.gitlab_issue_tracker_service.delete_all + end + end + + def down + # no-op + end +end diff --git a/db/structure.sql b/db/structure.sql index 758a0414d9c..4cf825da8c9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23812,6 +23812,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200623121135 20200623141217 20200623141544 +20200623142159 20200623170000 20200623185440 20200624075411 diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 6a8037ffd66..1f97cd304f9 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -137,7 +137,7 @@ We will note in the instructions below where these secrets are required. ### PostgreSQL NOTE: **Note:** -do not store the GitLab application database and the Praefect +Do not store the GitLab application database and the Praefect database on the same PostgreSQL server if using [Geo](../geo/replication/index.md). The replication state is internal to each instance of GitLab and should not be replicated. diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 551b17a2d7b..e93dfed3b1f 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -129,6 +129,7 @@ The following API resources are available outside of project and group contexts | [Geo Nodes](geo_nodes.md) **(PREMIUM ONLY)** | `/geo_nodes` | | [Group Activity Analytics](group_activity_analytics.md) **(STARTER)** | `/analytics/group_activity/{issues_count | merge_requests_count | new_members_count }` | | [Import repository from GitHub](import.md) | `/import/github` | +| [Instance clusters](instance_clusters.md) | `/admin/clusters` | | [Issues](issues.md) | `/issues` (also available for groups and projects) | | [Issues Statistics](issues_statistics.md) | `/issues_statistics` (also available for groups and projects) | | [Keys](keys.md) | `/keys` | diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index d0d68a403eb..aaee2d99968 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -603,6 +603,61 @@ type AlertSetAssigneesPayload { The issue created after mutation """ issue: Issue + + """ + The todo after mutation + """ + todo: Todo +} + +""" +Autogenerated input type of AlertTodoCreate +""" +input AlertTodoCreateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The iid of the alert to mutate + """ + iid: String! + + """ + The project the alert to mutate is in + """ + projectPath: ID! +} + +""" +Autogenerated return type of AlertTodoCreate +""" +type AlertTodoCreatePayload { + """ + The alert after mutation + """ + alert: AlertManagementAlert + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! + + """ + The issue created after mutation + """ + issue: Issue + + """ + The todo after mutation + """ + todo: Todo } """ @@ -1575,6 +1630,11 @@ type CreateAlertIssuePayload { The issue created after mutation """ issue: Issue + + """ + The todo after mutation + """ + todo: Todo } """ @@ -8114,6 +8174,7 @@ type Mutation { addProjectToSecurityDashboard(input: AddProjectToSecurityDashboardInput!): AddProjectToSecurityDashboardPayload adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload alertSetAssignees(input: AlertSetAssigneesInput!): AlertSetAssigneesPayload + alertTodoCreate(input: AlertTodoCreateInput!): AlertTodoCreatePayload awardEmojiAdd(input: AwardEmojiAddInput!): AwardEmojiAddPayload awardEmojiRemove(input: AwardEmojiRemoveInput!): AwardEmojiRemovePayload awardEmojiToggle(input: AwardEmojiToggleInput!): AwardEmojiTogglePayload @@ -13340,6 +13401,11 @@ enum TodoStateEnum { enum TodoTargetEnum { """ + An Alert + """ + ALERT + + """ A Commit """ COMMIT @@ -13660,6 +13726,11 @@ type UpdateAlertStatusPayload { The issue created after mutation """ issue: Issue + + """ + The todo after mutation + """ + todo: Todo } """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 02394f89ed1..46bfb0566ae 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -1460,6 +1460,164 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "todo", + "description": "The todo after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Todo", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AlertTodoCreateInput", + "description": "Autogenerated input type of AlertTodoCreate", + "fields": null, + "inputFields": [ + { + "name": "projectPath", + "description": "The project the alert to mutate is in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The iid of the alert to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AlertTodoCreatePayload", + "description": "Autogenerated return type of AlertTodoCreate", + "fields": [ + { + "name": "alert", + "description": "The alert after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "AlertManagementAlert", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issue", + "description": "The issue created after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "todo", + "description": "The todo after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Todo", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -4177,6 +4335,20 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "todo", + "description": "The todo after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Todo", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -22855,6 +23027,33 @@ "deprecationReason": null }, { + "name": "alertTodoCreate", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AlertTodoCreateInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AlertTodoCreatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "awardEmojiAdd", "description": null, "args": [ @@ -39509,6 +39708,12 @@ "deprecationReason": null }, { + "name": "ALERT", + "description": "An Alert", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "EPIC", "description": "An Epic", "isDeprecated": false, @@ -40394,6 +40599,20 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "todo", + "description": "The todo after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Todo", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7c2b7573f4a..b457da65d10 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -101,6 +101,19 @@ Autogenerated return type of AlertSetAssignees | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation | +| `todo` | Todo | The todo after mutation | + +## AlertTodoCreatePayload + +Autogenerated return type of AlertTodoCreate + +| Name | Type | Description | +| --- | ---- | ---------- | +| `alert` | AlertManagementAlert | The alert after mutation | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | +| `issue` | Issue | The issue created after mutation | +| `todo` | Todo | The todo after mutation | ## AwardEmoji @@ -274,6 +287,7 @@ Autogenerated return type of CreateAlertIssue | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation | +| `todo` | Todo | The todo after mutation | ## CreateAnnotationPayload @@ -2059,6 +2073,7 @@ Autogenerated return type of UpdateAlertStatus | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `issue` | Issue | The issue created after mutation | +| `todo` | Todo | The todo after mutation | ## UpdateContainerExpirationPolicyPayload diff --git a/doc/api/instance_clusters.md b/doc/api/instance_clusters.md new file mode 100644 index 00000000000..1108550eee7 --- /dev/null +++ b/doc/api/instance_clusters.md @@ -0,0 +1,293 @@ +# Instance clusters API + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36001) in GitLab 13.2. + +NOTE: **Note:** +User will need admin access to use these endpoints. + +Use these API endpoints with your instance clusters, which enable you to use the same cluster across multiple projects. [More information](../user/instance/clusters/index.md) + +## List instance clusters + +Returns a list of instance clusters. + +```plaintext +GET /admin/clusters +``` + +Example request: + +```shell +curl --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/admin/clusters" +``` + +Example response: + +```json +[ + { + "id": 9, + "name": "cluster-1", + "created_at": "2020-07-14T18:36:10.440Z", + "domain": null, + "provider_type": "user", + "platform_type": "kubernetes", + "environment_scope": "*", + "cluster_type": "instance_type", + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "platform_kubernetes": { + "api_url": "https://example.com", + "namespace": null, + "authorization_type": "rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----IxMDM1MV0ZDJkZjM...-----END CERTIFICATE-----" + }, + "provider_gcp": null, + "management_project": null + }, + { + "id": 10, + "name": "cluster-2", + "created_at": "2020-07-14T18:39:05.383Z", + "domain": null, + "provider_type": "user", + "platform_type": "kubernetes", + "environment_scope": "staging", + "cluster_type": "instance_type", + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "platform_kubernetes": { + "api_url": "https://example.com", + "namespace": null, + "authorization_type": "rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----LzEtMCadtaLGxcsGAZjM...-----END CERTIFICATE-----" + }, + "provider_gcp": null, + "management_project": null + } + { + "id": 11, + "name": "cluster-3", + ... + } +] + +``` + +## Get a single instance cluster + +Returns a single instance cluster. + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cluster_id` | integer | yes | The ID of the cluster | + +```plaintext +GET /admin/clusters/:cluster_id +``` + +Example request: + +```shell +curl --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/admin/clusters/9" +``` + +Example response: + +```json +{ + "id": 9, + "name": "cluster-1", + "created_at": "2020-07-14T18:36:10.440Z", + "domain": null, + "provider_type": "user", + "platform_type": "kubernetes", + "environment_scope": "*", + "cluster_type": "instance_type", + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "platform_kubernetes": { + "api_url": "https://example.com", + "namespace": null, + "authorization_type": "rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----IxMDM1MV0ZDJkZjM...-----END CERTIFICATE-----" + }, + "provider_gcp": null, + "management_project": null +} +``` + +## Add existing instance cluster + +Adds an existing Kubernetes instance cluster. + +```plaintext +POST /admin/clusters/add +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | yes | The name of the cluster | +| `domain` | string | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster | +| `environment_scope` | string | no | The associated environment to the cluster. Defaults to `*` | +| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster | +| `enabled` | boolean | no | Determines if cluster is active or not, defaults to true | +| `managed` | boolean | no | Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true | +| `platform_kubernetes_attributes[api_url]` | string | yes | The URL to access the Kubernetes API | +| `platform_kubernetes_attributes[token]` | string | yes | The token to authenticate against Kubernetes | +| `platform_kubernetes_attributes[ca_cert]` | string | no | TLS certificate. Required if API is using a self-signed TLS certificate. | +| `platform_kubernetes_attributes[namespace]` | string | no | The unique namespace related to the project | +| `platform_kubernetes_attributes[authorization_type]` | string | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. | + +Example request: + +```shell +curl --header "Private-Token:<your_access_token>" "http://gitlab.example.com/api/v4/admin/clusters/add" \ +-H "Accept:application/json" \ +-H "Content-Type:application/json" \ +-X POST --data '{"name":"cluster-3", "environment_scope":"production", "platform_kubernetes_attributes":{"api_url":"https://example.com", "token":"12345", "ca_cert":"-----BEGIN CERTIFICATE-----qpoeiXXZafCM0ZDJkZjM...-----END CERTIFICATE-----"}}' + +``` + +Example response: + +```json +{ + "id": 11, + "name": "cluster-3", + "created_at": "2020-07-14T18:42:50.805Z", + "domain": null, + "provider_type": "user", + "platform_type": "kubernetes", + "environment_scope": "production", + "cluster_type": "instance_type", + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com:3000/root" + }, + "platform_kubernetes": { + "api_url": "https://example.com", + "namespace": null, + "authorization_type": "rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----qpoeiXXZafCM0ZDJkZjM...-----END CERTIFICATE-----" + }, + "provider_gcp": null, + "management_project": null +} +``` + +## Edit instance cluster + +Updates an existing instance cluster. + +```shell +PUT /admin/clusters/:cluster_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cluster_id` | integer | yes | The ID of the cluster | +| `name` | string | no | The name of the cluster | +| `domain` | string | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster | +| `environment_scope` | string | no | The associated environment to the cluster | +| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster | +| `enabled` | boolean | no | Determines if cluster is active or not, defaults to true | +| `platform_kubernetes_attributes[api_url]` | string | no | The URL to access the Kubernetes API | +| `platform_kubernetes_attributes[token]` | string | no | The token to authenticate against Kubernetes | +| `platform_kubernetes_attributes[ca_cert]` | string | no | TLS certificate. Required if API is using a self-signed TLS certificate. | +| `platform_kubernetes_attributes[namespace]` | string | no | The unique namespace related to the project | + +NOTE: **Note:** +`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added +through the [Add existing Kubernetes cluster](../user/project/clusters/add_remove_clusters.md#add-existing-cluster) option or +through the [Add existing instance cluster](#add-existing-instance-cluster) endpoint. + +Example request: + +```shell +curl --header "Private-Token: <your_access_token>" "http://gitlab.example.com/api/v4/admin/clusters/9" \ +-H "Content-Type:application/json" \ +-X PUT --data '{"name":"update-cluster-name", "platform_kubernetes_attributes":{"api_url":"https://new-example.com","token":"new-token"}}' + +``` + +Example response: + +```json +{ + "id": 9, + "name": "update-cluster-name", + "created_at": "2020-07-14T18:36:10.440Z", + "domain": null, + "provider_type": "user", + "platform_type": "kubernetes", + "environment_scope": "*", + "cluster_type": "instance_type", + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "platform_kubernetes": { + "api_url": "https://new-example.com", + "namespace": null, + "authorization_type": "rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----IxMDM1MV0ZDJkZjM...-----END CERTIFICATE-----" + }, + "provider_gcp": null, + "management_project": null, + "project": null +} + +``` + +## Delete instance cluster + +Deletes an existing instance cluster. + +```plaintext +DELETE /admin/clusters/:cluster_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cluster_id` | integer | yes | The ID of the cluster | + +Example request: + +```shell +curl --request DELETE --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/admin/clusters/11" +``` diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 89206ff8e06..21c99f928d8 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -6,6 +6,8 @@ type: reference --- # Configuring GitLab Runners +<!-- This topic contains several commented-out sections that were accidentally added in 13.2.--> +<!-- The commented-out sections will be added back in 13.3.--> In GitLab CI/CD, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md). A GitLab Runner is a lightweight, highly-scalable agent that picks up a CI job through @@ -37,9 +39,11 @@ multiple projects. If you are using a self-managed instance of GitLab: -- Your administrator can install and register shared Runners by going to your project's - **Settings > CI / CD**, expanding the **Runners** section, and clicking **Show Runner installation instructions**. - These instructions are also available [here](https://docs.gitlab.com/runner/install/index.html). +- Your administrator can install and register shared Runners by viewing the instructions + [here](https://docs.gitlab.com/runner/install/index.html). + <!-- going to your project's + <!-- **Settings > CI / CD**, expanding the **Runners** section, and clicking **Show Runner installation instructions**.--> + <!-- These instructions are also available [here](https://docs.gitlab.com/runner/install/index.html).--> - The administrator can also configure a maximum number of shared Runner [pipeline minutes for each group](../../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota-starter-only). @@ -119,22 +123,21 @@ To enable shared Runners: #### Disable shared Runners -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23123) for groups in GitLab 13.2. - -You can disable shared Runners for individual projects or for groups. -You must have Owner permissions for the project or group. +You can disable shared Runners for individual projects<!-- or for groups-->. +You must have Owner permissions for the project<!-- or group-->. To disable shared Runners for a project: 1. Go to the project's **{settings}** **Settings > CI/CD** and expand the **Runners** section. 1. In the **Shared Runners** area, click **Disable shared Runners**. -To disable shared Runners for a group: +<!--To disable shared Runners for a group: 1. Go to the group's **{settings}** **Settings > CI/CD** and expand the **Runners** section. 1. In the **Shared Runners** area, click **Disable shared Runners globally**. 1. Optionally, to allow shared Runners to be enabled for individual projects or subgroups, click **Allow projects/subgroups to override the global setting**. +--> ### Group Runners @@ -156,9 +159,9 @@ To create a group Runner: 1. Note the URL and token. 1. [Register the Runner](https://docs.gitlab.com/runner/register/). -#### View and manage group Runners +<!-- #### View and manage group Runners -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/37366/) in GitLab 13.2. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/37366/) in GitLab 13.3. You can view and manage all Runners for a group, its subgroups, and projects. You can do this for your self-managed GitLab instance or for GitLab.com. @@ -180,7 +183,7 @@ You must have [Owner permissions](../../user/permissions.md#group-members-permis | Tags | Tags associated with the Runner | | Last contact | Timestamp indicating when the GitLab instance last contacted the Runner | -From this page, you can edit, pause, and remove Runners from the group, its subgroups, and projects. +From this page, you can edit, pause, and remove Runners from the group, its subgroups, and projects. --> #### Pause or remove a group Runner @@ -190,9 +193,9 @@ You must have [Owner permissions](../../user/permissions.md#group-members-permis 1. Go to the group you want to remove or pause the Runner for. 1. Go to **{settings}** **Settings > CI/CD** and expand the **Runners** section. 1. Click **Pause** or **Remove Runner**. - - If you pause a group Runner that is used by multiple projects, the Runner pauses for all projects. - - From the group view, you cannot remove a Runner that is assigned to more than one project. - You must remove it from each project first. +<!-- - If you pause a group Runner that is used by multiple projects, the Runner pauses for all projects. --> +<!-- - From the group view, you cannot remove a Runner that is assigned to more than one project. --> +<!-- You must remove it from each project first. --> 1. On the confirmation dialog, click **OK**. ### Specific Runners diff --git a/doc/development/database_review.md b/doc/development/database_review.md index f864f13f489..967df411db5 100644 --- a/doc/development/database_review.md +++ b/doc/development/database_review.md @@ -19,6 +19,10 @@ A database review is required for: generally up to the author of a merge request to decide whether or not complex queries are being introduced and if they require a database review. +- Changes in usage data metrics that use `count` and `distinct_count`. + These metrics could have complex queries over large tables. + See the [Telemetry Guide](telemetry/usage_ping.md#implementing-usage-ping) + for implementation details. A database reviewer is expected to look out for obviously complex queries in the change and review those closer. If the author does not diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index 60f0823536e..8b4e5090abb 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -1,7 +1,14 @@ -# GitLab Developers Guide to Working with Gitaly +--- +stage: Create +group: Gitaly +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +type: reference +--- + +# Gitaly developers guide -[Gitaly](https://gitlab.com/gitlab-org/gitaly) is a high-level Git RPC service used by GitLab CE/EE, -Workhorse and GitLab-Shell. +[Gitaly](https://gitlab.com/gitlab-org/gitaly) is a high-level Git RPC service used by GitLab Rails, +Workhorse and GitLab Shell. ## Deep Dive diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index f862630deb5..35c046423b0 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -546,7 +546,8 @@ gitlab=# \q #### Set up Gitaly CAUTION: **Caution:** -In this architecture, having a single Gitaly server creates a single point of failure. This limitation will be removed once [Gitaly Cluster](https://gitlab.com/groups/gitlab-org/-/epics/1489) is released. +In this architecture, having a single Gitaly server creates a single point of failure. Use +[Gitaly Cluster](../../administration/gitaly/praefect.md) to remove this limitation. Gitaly is a service that provides high-level RPC access to Git repositories. It should be enabled and configured on a separate EC2 instance in one of the diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md index 61e730ce09b..229a8572206 100644 --- a/doc/user/application_security/configuration/index.md +++ b/doc/user/application_security/configuration/index.md @@ -21,7 +21,7 @@ state of each feature. If a job with the expected security report artifact exist the feature is considered configured. NOTE: **Note:** -if the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md), +If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md), all security features will be configured by default. ## Limitations diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md index 6909941f398..214044ad783 100644 --- a/doc/user/application_security/sast/analyzers.md +++ b/doc/user/application_security/sast/analyzers.md @@ -32,7 +32,6 @@ SAST supports the following official analyzers: - [`security-code-scan`](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan) (Security Code Scan (.NET)) - [`sobelow`](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow) (Sobelow (Elixir Phoenix)) - [`spotbugs`](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) (SpotBugs with the Find Sec Bugs plugin (Ant, Gradle and wrapper, Grails, Maven and wrapper, SBT)) -- [`tslint`](https://gitlab.com/gitlab-org/security-products/analyzers/tslint) (TSLint (TypeScript)) The analyzers are published as Docker images that SAST will use to launch dedicated containers for each analysis. @@ -145,24 +144,24 @@ The [Security Scanner Integration](../../../development/integrations/secure.md) ## Analyzers Data -| Property \ Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Sobelow | TSLint Security | -| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :----------------: | :-------------: | -| Severity | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | ✓ | -| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Description | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | -| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | ✓ | -| End line | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | -| Start column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | -| End column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | -| External ID (e.g. CVE) | 𐄂 | 𐄂 | ⚠ | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| URLs | ✓ | 𐄂 | ✓ | 𐄂 | ⚠ | 𐄂 | ⚠ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Internal doc/explanation | ✓ | ⚠ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | 𐄂 | -| Solution | ✓ | 𐄂 | 𐄂 | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Affected item (e.g. class or package) | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Confidence | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | ✓ | 𐄂 | -| Source code extract | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | +| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Sobelow | +| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :----------------: | +| Severity | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | +| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Description | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | +| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | +| End line | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Start column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | +| End column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| External ID (e.g. CVE) | 𐄂 | 𐄂 | ⚠ | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| URLs | ✓ | 𐄂 | ✓ | 𐄂 | ⚠ | 𐄂 | ⚠ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Internal doc/explanation | ✓ | ⚠ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | +| Solution | ✓ | 𐄂 | 𐄂 | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Affected item (e.g. class or package) | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Confidence | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | ✓ | +| Source code extract | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | - ✓ => we have that data - ⚠ => we have that data but it's partially reliable, or we need to extract it from unstructured content diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 14b1d86c78c..70d4b513cf9 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -71,15 +71,15 @@ The following table shows which languages, package managers and frameworks are s | Language (package managers) / framework | Scan tool | Introduced in GitLab Version | |-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------| -| .NET Core | [Security Code Scan](https://security-code-scan.github.io) | 11.0 | -| .NET Framework | [Security Code Scan](https://security-code-scan.github.io) | 13.0 | -| Any | [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) | 11.9 | -| Apex (Salesforce) | [PMD](https://pmd.github.io/pmd/index.html) | 12.1 | -| C/C++ | [Flawfinder](https://github.com/david-a-wheeler/flawfinder) | 10.7 | -| Elixir (Phoenix) | [Sobelow](https://github.com/nccgroup/sobelow) | 11.10 | -| Go | [Gosec](https://github.com/securego/gosec) | 10.7 | -| Groovy ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.3 (Gradle) & 11.9 (Ant, Maven, SBT) | -| Helm Charts | [Kubesec](https://github.com/controlplaneio/kubesec) | 13.1 | +| .NET Core | [Security Code Scan](https://security-code-scan.github.io) | 11.0 | +| .NET Framework | [Security Code Scan](https://security-code-scan.github.io) | 13.0 | +| Any | [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) | 11.9 | +| Apex (Salesforce) | [PMD](https://pmd.github.io/pmd/index.html) | 12.1 | +| C/C++ | [Flawfinder](https://github.com/david-a-wheeler/flawfinder) | 10.7 | +| Elixir (Phoenix) | [Sobelow](https://github.com/nccgroup/sobelow) | 11.10 | +| Go | [Gosec](https://github.com/securego/gosec) | 10.7 | +| Groovy ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.3 (Gradle) & 11.9 (Ant, Maven, SBT) | +| Helm Charts | [Kubesec](https://github.com/controlplaneio/kubesec) | 13.1 | | Java ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) | | JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8, moved to [GitLab Core](https://about.gitlab.com/pricing/) in 13.2 | | Kubernetes manifests | [Kubesec](https://github.com/controlplaneio/kubesec) | 12.6 | @@ -89,7 +89,7 @@ The following table shows which languages, package managers and frameworks are s | React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 | | Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3, moved to [GitLab Core](https://about.gitlab.com/pricing/) in 13.1 | | Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) | -| TypeScript | [`tslint-config-security`](https://github.com/webschik/tslint-config-security/) | 11.9 | +| TypeScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.9, merged with ESLint in 13.2 | NOTE: **Note:** The Java analyzers can also be used for variants like the @@ -529,7 +529,6 @@ registry.gitlab.com/gitlab-org/security-products/analyzers/secrets:2 registry.gitlab.com/gitlab-org/security-products/analyzers/security-code-scan:2 registry.gitlab.com/gitlab-org/security-products/analyzers/sobelow:2 registry.gitlab.com/gitlab-org/security-products/analyzers/spotbugs:2 -registry.gitlab.com/gitlab-org/security-products/analyzers/tslint:2 ``` The process for importing Docker images into a local offline Docker registry depends on diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 090f708d6e1..507ba25850d 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -1495,6 +1495,12 @@ NOTE: **Note:** Support for installing the AppArmor managed application is provided by the GitLab Container Security group. If you run into unknown issues, please [open a new issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new) and ping at least 2 people from the [Container Security group](https://about.gitlab.com/handbook/product/product-categories/#container-security-group). +## Browse applications logs + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36769) in GitLab 13.2. + +Logs produced by pods running **GitLab Managed Apps** can be browsed using [**Log Explorer**](../project/clusters/kubernetes_pod_logs.md). + ## Upgrading applications > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24789) in GitLab 11.8. diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md index 06cd290f5ad..e24e669d994 100644 --- a/doc/user/infrastructure/index.md +++ b/doc/user/infrastructure/index.md @@ -117,19 +117,21 @@ and the CI YAML file: ``` 1. In the `.gitlab-ci.yaml` file, define some environment variables to ease - development. In this example, `TF_STATE` is the name of the Terraform state - (projects may have multiple states), `TF_ADDRESS` is the URL to the state on - the GitLab instance where this pipeline runs, and `TF_ROOT` is the directory - where the Terraform commands must be executed: + development. In this example, `TF_ROOT` is the directory where the Terraform + commands must be executed, `TF_ADDRESS` is the URL to the state on the GitLab + instance where this pipeline runs, and the final path segment in `TF_ADDRESS` + is the name of the Terraform state. Projects may have multiple states, and + this name is arbitrary, so in this example we will set it to the name of the + project, and we will ensure that the `.terraform` directory is cached between + jobs in the pipeline using a cache key based on the state name: ```yaml variables: - TF_STATE: ${CI_PROJECT_NAME} - TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE} TF_ROOT: ${CI_PROJECT_DIR}/environments/cloudflare/production + TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PROJECT_NAME} cache: - key: ${TF_STATE} + key: ${CI_PROJECT_NAME} paths: - ${TF_ROOT}/.terraform ``` @@ -273,12 +275,11 @@ can configure this manually as follows: image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest variables: - TF_STATE: ${CI_PROJECT_NAME} - TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE} TF_ROOT: ${CI_PROJECT_DIR}/environments/cloudflare/production + TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PROJECT_NAME} cache: - key: ${TF_STATE} + key: ${CI_PROJECT_NAME} paths: - ${TF_ROOT}/.terraform diff --git a/doc/user/project/clusters/securing.md b/doc/user/project/clusters/securing.md index 22dc4eb8106..b4c20cb8dbc 100644 --- a/doc/user/project/clusters/securing.md +++ b/doc/user/project/clusters/securing.md @@ -1,6 +1,6 @@ --- stage: Defend -group: container_security +group: Container Security info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index 9867af8976a..79c55e2d140 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -14,8 +14,6 @@ want to configure. ![Integrations list](img/project_services.png) -Below, you will find a list of the currently supported ones accompanied with comprehensive documentation. - ## Integrations listing Click on the service links to see further configuration instructions and details. @@ -69,16 +67,16 @@ supported by `push_hooks` and `tag_push_hooks` events won't be executed. The number of branches or tags supported can be changed via [`push_event_hooks_limit` application setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls). -## Services templates +## Service templates -Services templates is a way to set some predefined values in the Service of -your liking which will then be pre-filled on each project's Service. +Service templates are a way to set predefined values for an integration across +all new projects on the instance. -Read more about [Services templates in this document](services_templates.md). +Read more about [Service templates in this document](services_templates.md). ## Troubleshooting integrations -Some integrations use service hooks for integration with external applications. To confirm which ones use service hooks, see the [integrations listing](#integrations-listing). GitLab stores details of service hook requests made within the last 2 days. To view details of the requests, go to the service's configuration page. +Some integrations use service hooks for integration with external applications. To confirm which ones use service hooks, see the [integrations listing](#integrations-listing) above. GitLab stores details of service hook requests made within the last 2 days. To view details of the requests, go to that integration's configuration page. The **Recent Deliveries** section lists the details of each request made within the last 2 days: @@ -89,17 +87,17 @@ The **Recent Deliveries** section lists the details of each request made within - Relative time in which the request was made To view more information about the request's execution, click the respective **View details** link. -On the details page, you can see the data sent by GitLab (request headers and body) and the data received by GitLab (response headers and body). +On the details page, you can see the request headers and body sent and received by GitLab. -From this page, you can repeat delivery with the same data by clicking **Resend Request**. +To repeat a delivery using the same data, click **Resend Request**. ![Recent deliveries](img/webhook_logs.png) ### Uninitialized repositories Some integrations fail with an error `Test Failed. Save Anyway` when you attempt to set them up on -uninitialized repositories. This is because the default service test uses push data to build the -payload for the test request, and it fails, because there are no push events for the project. +uninitialized repositories. Some integrations use push data to build the test payload, +and this error occurs when no push events exist in the project yet. To resolve this error, initialize the repository by pushing a test file to the project and set up the integration again. diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md index 8a88df88629..bc2bdde2f64 100644 --- a/doc/user/project/integrations/services_templates.md +++ b/doc/user/project/integrations/services_templates.md @@ -1,28 +1,25 @@ -# Services templates +# Service templates -A GitLab administrator can add a service template that sets a default for each -project. After a service template is enabled, it will be applied to **all** -projects that don't have it already enabled and its details will be pre-filled -on the project's Service page. By disabling the template, it will be disabled -for new projects only. +Using a service template, GitLab administrators can provide default values for configuring integrations at the project level. + +When you enable a service template, the defaults are applied to **all** projects that do not +already have the integration enabled or do not otherwise have custom values saved. +The values are pre-filled on each project's configuration page for the applicable integration. + +If you disable the template, these values no longer appear as defaults, while +any values already saved for an integration remain unchanged. ## Enable a service template Navigate to the **Admin Area > Service Templates** and choose the service template you wish to create. -## Services for external issue trackers +## Service for external issue trackers -In the image below you can see how a service template for Redmine would look -like. +The following image shows an example service template for Redmine. ![Redmine service template](img/services_templates_redmine_example.png) ---- - For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used -by your external issue tracker. Prior to GitLab v7.8, this ID was configured in -the project settings, and GitLab would automatically update the URL configured -in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs -must be configured directly within the project's **Integrations** settings. +by your external issue tracker. diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index f1c3ef3fa5c..39adb9d0f60 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -60,6 +60,15 @@ and [PDFs](https://gitlab.com/gitlab-org/gitlab/-/issues/32811) is planned for a - Only the latest version of the designs can be deleted. - Deleted designs cannot be recovered but you can see them on previous designs versions. +## GitLab-Figma plugin + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-figma-plugin/-/issues/2) in GitLab 13.2. + +Connect your design environment with your source code management in a seamless workflow. The GitLab-Figma plugin makes it quick and easy to collaborate in GitLab by bringing the work of product designers directly from Figma to GitLab Issues as uploaded Designs. + +To use the plugin, install it from the [Figma Directory](https://www.figma.com/community/plugin/860845891704482356) +and connect to GitLab through a personal access token. The details are explained in the [plugin documentation](https://gitlab.com/gitlab-org/gitlab-figma-plugin/-/wikis/home). + ## The Design Management section > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223193) in GitLab 13.2, Designs are displayed directly on the issue description rather than on a separate tab. @@ -250,32 +259,10 @@ Feature.disable(:design_management_reference_filter_gfm_pipeline) ## Design activity records -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33051) in GitLab 13.1 -> - It's deployed behind a feature flag, disabled by default. -> - It's enabled on GitLab.com. -> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-design-events-core-only). **(CORE ONLY)** +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33051) in GitLab 13.1. +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/225205) in GitLab 13.2. User activity events on designs (creation, deletion, and updates) are tracked by GitLab and displayed on the [user profile](../../profile/index.md#user-profile), [group](../../group/index.md#view-group-activity), and [project](../index.md#project-activity) activity pages. - -### Enable or disable Design Events **(CORE ONLY)** - -User activity for designs is under development and not ready for production use. It is -deployed behind a feature flag that is **disabled by default**. -[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session) -can enable it for your instance. You're welcome to test it, but use it at your -own risk. - -To enable it: - -```ruby -Feature.enable(:design_activity_events) -``` - -To disable it: - -```ruby -Feature.disable(:design_activity_events) -``` diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb new file mode 100644 index 00000000000..8208d10c089 --- /dev/null +++ b/lib/api/admin/instance_clusters.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module API + module Admin + class InstanceClusters < Grape::API::Instance + include PaginationParams + + before do + authenticated_as_admin! + end + + namespace 'admin' do + desc "Get list of all instance clusters" do + detail "This feature was introduced in GitLab 13.2." + end + get '/clusters' do + authorize! :read_cluster, clusterable_instance + present paginate(clusters_for_current_user), with: Entities::Cluster + end + + desc "Get a single instance cluster" do + detail "This feature was introduced in GitLab 13.2." + end + params do + requires :cluster_id, type: Integer, desc: "The cluster ID" + end + get '/clusters/:cluster_id' do + authorize! :read_cluster, cluster + + present cluster, with: Entities::Cluster + end + + desc "Add an instance cluster" do + detail "This feature was introduced in GitLab 13.2." + end + params do + requires :name, type: String, desc: 'Cluster name' + optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true' + optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster' + optional :domain, type: String, desc: 'Cluster base domain' + optional :management_project_id, type: Integer, desc: 'The ID of the management project' + optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true' + requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do + requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API' + requires :token, type: String, desc: 'Token to authenticate against Kubernetes' + optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)' + optional :namespace, type: String, desc: 'Unique namespace related to Project' + optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' + end + end + post '/clusters/add' do + authorize! :add_cluster, clusterable_instance + + user_cluster = ::Clusters::CreateService + .new(current_user, create_cluster_user_params) + .execute + + if user_cluster.persisted? + present user_cluster, with: Entities::Cluster + else + render_validation_error!(user_cluster) + end + end + + desc "Update an instance cluster" do + detail "This feature was introduced in GitLab 13.2." + end + params do + requires :cluster_id, type: Integer, desc: 'The cluster ID' + optional :name, type: String, desc: 'Cluster name' + optional :enabled, type: Boolean, desc: 'Enable or disable Gitlab\'s connection to your Kubernetes cluster' + optional :environment_scope, type: String, desc: 'The associated environment to the cluster' + optional :domain, type: String, desc: 'Cluster base domain' + optional :management_project_id, type: Integer, desc: 'The ID of the management project' + optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do + optional :api_url, type: String, desc: 'URL to access the Kubernetes API' + optional :token, type: String, desc: 'Token to authenticate against Kubernetes' + optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)' + optional :namespace, type: String, desc: 'Unique namespace related to Project' + end + end + put '/clusters/:cluster_id' do + authorize! :update_cluster, cluster + + update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params) + + if update_service.execute(cluster) + present cluster, with: Entities::ClusterProject + else + render_validation_error!(cluster) + end + end + + desc "Remove a cluster" do + detail "This feature was introduced in GitLab 13.2." + end + params do + requires :cluster_id, type: Integer, desc: "The cluster ID" + end + delete '/clusters/:cluster_id' do + authorize! :admin_cluster, cluster + + destroy_conditionally!(cluster) + end + end + + helpers do + def clusterable_instance + Clusters::Instance.new + end + + def clusters_for_current_user + @clusters_for_current_user ||= ClustersFinder.new(clusterable_instance, current_user, :all).execute + end + + def cluster + @cluster ||= clusters_for_current_user.find(params[:cluster_id]) + end + + def create_cluster_user_params + declared_params.merge({ + provider_type: :user, + platform_type: :kubernetes, + clusterable: clusterable_instance + }) + end + + def update_cluster_params + declared_params(include_missing: false).without(:cluster_id) + end + end + end + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index 41fc36b0c14..5fccc210779 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -125,6 +125,7 @@ module API # Keep in alphabetical order mount ::API::AccessRequests mount ::API::Admin::Ci::Variables + mount ::API::Admin::InstanceClusters mount ::API::Admin::Sidekiq mount ::API::Appearance mount ::API::Applications diff --git a/lib/event_filter.rb b/lib/event_filter.rb index 73bdc8f0649..0b5833b91ed 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -26,8 +26,6 @@ class EventFilter # rubocop: disable CodeReuse/ActiveRecord def apply_filter(events) - events = apply_feature_flags(events) - case filter when PUSH events.pushed_action @@ -51,29 +49,17 @@ class EventFilter private - def apply_feature_flags(events) - events = events.not_design unless can_view_design_activity? - - events - end - def wiki_events(events) events.for_wiki_page end def design_events(events) - return events.for_design if can_view_design_activity? - - events + events.for_design end def filters [ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM, WIKI, DESIGNS] end - - def can_view_design_activity? - Feature.enabled?(:design_activity_events) - end end EventFilter.prepend_if_ee('EE::EventFilter') diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 01a2340612c..f0e2f48dd5c 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -9,7 +9,7 @@ variables: # (SAST, Dependency Scanning, ...) SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers" - SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex, kubesec" + SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, secrets, sobelow, pmd-apex, kubesec" SAST_EXCLUDED_PATHS: "spec, test, tests, tmp" SAST_ANALYZER_IMAGE_TAG: 2 SAST_DISABLE_DIND: "true" @@ -95,6 +95,8 @@ eslint-sast: - '**/*.html' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' flawfinder-sast: extends: .sast-analyzer @@ -226,16 +228,3 @@ spotbugs-sast: - '**/*.groovy' - '**/*.java' - '**/*.scala' - -tslint-sast: - extends: .sast-analyzer - image: - name: "$SECURE_ANALYZERS_PREFIX/tslint:$SAST_ANALYZER_IMAGE_TAG" - rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' - when: never - - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bsast\b/ && - $SAST_DEFAULT_ANALYZERS =~ /tslint/ - exists: - - '**/*.ts' diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml index d6cc446b9fc..2d2e0859373 100644 --- a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml @@ -13,7 +13,7 @@ variables: SECURE_BINARIES_ANALYZERS: >- - bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex, kubesec, + bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, secrets, sobelow, pmd-apex, kubesec, bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python, klar, clair-vulnerabilities-db, license-finder, @@ -125,13 +125,6 @@ eslint: - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\beslint\b/ -tslint: - extends: .download_images - only: - variables: - - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && - $SECURE_BINARIES_ANALYZERS =~ /\btslint\b/ - secrets: extends: .download_images only: diff --git a/lib/gitlab/updated_notes_paginator.rb b/lib/gitlab/updated_notes_paginator.rb new file mode 100644 index 00000000000..3d3d0e5bf9e --- /dev/null +++ b/lib/gitlab/updated_notes_paginator.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gitlab + # UpdatedNotesPaginator implements a rudimentary form of keyset pagination on + # top of a notes relation that has been initialized with a `last_fetched_at` + # value. This class will attempt to limit the number of notes returned, and + # specify a new value for `last_fetched_at` that will pick up where the last + # page of notes left off. + class UpdatedNotesPaginator + LIMIT = 50 + MICROSECOND = 1_000_000 + + attr_reader :next_fetched_at, :notes + + def initialize(relation, last_fetched_at:) + @last_fetched_at = last_fetched_at + @now = Time.current + + notes, more = fetch_page(relation) + if more + init_middle_page(notes) + else + init_final_page(notes) + end + end + + def metadata + { last_fetched_at: next_fetched_at_microseconds, more: more } + end + + private + + attr_reader :last_fetched_at, :more, :now + + def next_fetched_at_microseconds + (next_fetched_at.to_i * MICROSECOND) + next_fetched_at.usec + end + + def fetch_page(relation) + relation = relation.by_updated_at + notes = relation.at_most(LIMIT + 1).to_a + + return [notes, false] unless notes.size > LIMIT + + marker = notes.pop # Remove the marker note + + # Although very unlikely, it is possible that more notes with the same + # updated_at may exist, e.g., if created in bulk. Add them all to the page + # if this is detected, so pagination won't get stuck indefinitely + if notes.last.updated_at == marker.updated_at + notes += relation + .with_updated_at(marker.updated_at) + .id_not_in(notes.map(&:id)) + .to_a + end + + [notes, true] + end + + def init_middle_page(notes) + @more = true + + # The fetch overlap can be ignored if we're in an intermediate page. + @next_fetched_at = notes.last.updated_at + NotesFinder::FETCH_OVERLAP + @notes = notes + end + + def init_final_page(notes) + @more = false + @next_fetched_at = now + @notes = notes + end + end +end diff --git a/lib/product_analytics/collector_app.rb b/lib/product_analytics/collector_app.rb new file mode 100644 index 00000000000..cf971eef4b6 --- /dev/null +++ b/lib/product_analytics/collector_app.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module ProductAnalytics + class CollectorApp + def call(env) + request = Rack::Request.new(env) + params = request.params + + return not_found unless EventParams.has_required_params?(params) + + # Product analytics feature is behind a flag and is disabled by default. + # We expect limited amount of projects with this feature enabled in first release. + # Since collector has no authentication we temporary prevent recording of events + # for project without the feature enabled. During increase of feature adoption, this + # check will be removed for better performance. + project = Project.find(params['aid'].to_i) + return not_found unless Feature.enabled?(:product_analytics, project, default_enabled: false) + + # Snowplow tracker has own format of events. + # We need to convert them to match the schema of our database. + event_params = EventParams.parse_event_params(params) + + if ProductAnalyticsEvent.create(event_params) + ok + else + not_found + end + rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotFound + not_found + end + + def ok + [200, {}, []] + end + + def not_found + [404, {}, []] + end + end +end diff --git a/lib/product_analytics/event_params.rb b/lib/product_analytics/event_params.rb new file mode 100644 index 00000000000..d938fe1f594 --- /dev/null +++ b/lib/product_analytics/event_params.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module ProductAnalytics + # Converts params from Snowplow tracker to one compatible with + # GitLab ProductAnalyticsEvent model. The field naming corresponds + # with snowplow event model. Only project_id is GitLab specific. + # + # For information on what each field is you can check next resources: + # * Snowplow tracker protocol: https://github.com/snowplow/snowplow/wiki/snowplow-tracker-protocol + # * Canonical event model: https://github.com/snowplow/snowplow/wiki/canonical-event-model + class EventParams + def self.parse_event_params(params) + { + project_id: params['aid'], + platform: params['p'], + collector_tstamp: Time.zone.now, + event_id: params['eid'], + v_tracker: params['tv'], + v_collector: Gitlab::VERSION, + v_etl: Gitlab::VERSION, + os_timezone: params['tz'], + name_tracker: params['tna'], + br_lang: params['lang'], + doc_charset: params['cs'], + br_features_pdf: Gitlab::Utils.to_boolean(params['f_pdf']), + br_features_flash: Gitlab::Utils.to_boolean(params['f_fla']), + br_features_java: Gitlab::Utils.to_boolean(params['f_java']), + br_features_director: Gitlab::Utils.to_boolean(params['f_dir']), + br_features_quicktime: Gitlab::Utils.to_boolean(params['f_qt']), + br_features_realplayer: Gitlab::Utils.to_boolean(params['f_realp']), + br_features_windowsmedia: Gitlab::Utils.to_boolean(params['f_wma']), + br_features_gears: Gitlab::Utils.to_boolean(params['f_gears']), + br_features_silverlight: Gitlab::Utils.to_boolean(params['f_ag']), + br_colordepth: params['cd'], + br_cookies: Gitlab::Utils.to_boolean(params['cookie']), + dvce_created_tstamp: params['dtm'], + br_viewheight: params['vp'], + domain_sessionidx: params['vid'], + domain_sessionid: params['sid'], + domain_userid: params['duid'], + user_fingerprint: params['fp'], + page_referrer: params['refr'], + page_url: params['url'] + } + end + + def self.has_required_params?(params) + params['aid'].present? && params['eid'].present? + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 35afbe957f8..67148d60ecc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2106,6 +2106,9 @@ msgstr "" msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear." msgstr "" +msgid "AlertManagement|There was an error while updating the To Do of the alert." +msgstr "" + msgid "AlertManagement|There was an error while updating the assignee(s) list. Please try again." msgstr "" @@ -10357,9 +10360,6 @@ msgstr "" msgid "Filter pipelines" msgstr "" -msgid "Filter projects" -msgstr "" - msgid "Filter results" msgstr "" @@ -10567,18 +10567,6 @@ msgstr "" msgid "From <code>%{source_title}</code> into" msgstr "" -msgid "From Bitbucket" -msgstr "" - -msgid "From Bitbucket Server" -msgstr "" - -msgid "From FogBugz" -msgstr "" - -msgid "From GitLab.com" -msgstr "" - msgid "From Google Code" msgstr "" @@ -11053,9 +11041,6 @@ msgstr "" msgid "Getting started with releases" msgstr "" -msgid "Git" -msgstr "" - msgid "Git LFS is not enabled on this GitLab server, contact your admin." msgstr "" @@ -14546,6 +14531,9 @@ msgstr "" msgid "Merge request dependencies" msgstr "" +msgid "Merge request was scheduled to merge after pipeline succeeds" +msgstr "" + msgid "Merge requests" msgstr "" @@ -16249,9 +16237,6 @@ msgstr "" msgid "One or more of your %{provider} projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git." msgstr "" -msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git." -msgstr "" - msgid "One or more of your Google Code projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git." msgstr "" @@ -17305,9 +17290,6 @@ msgstr "" msgid "Please convert %{linkStart}them to Git%{linkEnd}, and go through the %{linkToImportFlow} again." msgstr "" -msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again." -msgstr "" - msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again." msgstr "" @@ -19234,9 +19216,6 @@ msgstr "" msgid "Queued" msgstr "" -msgid "Quick actions" -msgstr "" - msgid "Quick actions can be used in the issues description and comment boxes." msgstr "" @@ -20485,9 +20464,6 @@ msgstr "" msgid "Search" msgstr "" -msgid "Search Button" -msgstr "" - msgid "Search Jira issues" msgstr "" @@ -24801,9 +24777,6 @@ msgstr "" msgid "To start serving your jobs you can either add specific Runners to your project or use shared Runners" msgstr "" -msgid "To this GitLab instance" -msgstr "" - msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file" msgstr "" @@ -26826,6 +26799,9 @@ msgstr "" msgid "You" msgstr "" +msgid "You already have pending todo for this alert" +msgstr "" + msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application." msgstr "" @@ -27105,6 +27081,9 @@ msgstr "" msgid "You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues." msgstr "" +msgid "You have insufficient permissions to create a Todo for this alert" +msgstr "" + msgid "You have no permissions" msgstr "" @@ -28914,6 +28893,9 @@ msgstr "" msgid "vulnerability|dismissed" msgstr "" +msgid "was scheduled to merge after pipeline succeeds by" +msgstr "" + msgid "wiki page" msgstr "" diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index cf15cf9256b..c838affa239 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -55,20 +55,6 @@ RSpec.describe DashboardController do expect(json_response['count']).to eq(6) end - - describe 'design_activity_events feature flag' do - context 'it is off' do - before do - stub_feature_flags(design_activity_events: false) - end - - it 'excludes design activity' do - get :activity, params: { format: :json } - - expect(json_response['count']).to eq(4) - end - end - end end context 'when user has no permission to see the event' do diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 72204cff3ab..469e58c94e7 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -1164,18 +1164,6 @@ RSpec.describe GroupsController do expect(json_response['count']).to eq(3) end - - context 'the design_activity_events feature flag is disabled' do - before do - stub_feature_flags(design_activity_events: false) - end - - it 'does not include the design activity' do - get_activity - - expect(json_response['count']).to eq(1) - end - end end describe 'GET #issues' do diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index ec38a635c2d..0427715d1ac 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -58,12 +58,12 @@ RSpec.describe Import::BitbucketController do before do @repo = double(name: 'vim', slug: 'vim', owner: 'asd', full_name: 'asd/vim', clone_url: 'http://test.host/demo/url.git', 'valid?' => true) @invalid_repo = double(name: 'mercurialrepo', slug: 'mercurialrepo', owner: 'asd', full_name: 'asd/mercurialrepo', clone_url: 'http://test.host/demo/mercurialrepo.git', 'valid?' => false) + allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org') assign_session_tokens - stub_feature_flags(new_import_ui: false) end - it_behaves_like 'import controller with new_import_ui feature flag' do + it_behaves_like 'import controller status' do before do allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org') end @@ -75,44 +75,16 @@ RSpec.describe Import::BitbucketController do let(:client_repos_field) { :repos } end - context 'with new_import_ui feature flag enabled' do - before do - stub_feature_flags(new_import_ui: true) - allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org') - end - - it 'returns invalid repos' do - allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo, @invalid_repo]) - - get :status, format: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['incompatible_repos'].length).to eq(1) - expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name) - expect(json_response['provider_repos'].length).to eq(1) - expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name) - end - end - - it "assigns variables" do - @project = create(:project, import_type: 'bitbucket', creator_id: user.id) - allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) + it 'returns invalid repos' do + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo, @invalid_repo]) - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo]) - expect(assigns(:incompatible_repos)).to eq([]) - end + get :status, format: :json - it "does not show already added project" do - @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim') - allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['incompatible_repos'].length).to eq(1) + expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name) + expect(json_response['provider_repos'].length).to eq(1) + expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name) end context 'when filtering' do diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index 4aa70d399ce..bb80de6425f 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -148,28 +148,21 @@ RSpec.describe Import::BitbucketServerController do @invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo', name: 'invalid') @created_repo = double(slug: 'created', project_key: 'existing', full_name: 'group/created', "valid?" => true, browse_url: 'http://existing') assign_session_tokens - stub_feature_flags(new_import_ui: false) end - context 'with new_import_ui feature flag enabled' do - before do - stub_feature_flags(new_import_ui: true) - end - - it 'returns invalid repos' do - allow(client).to receive(:repos).with(filter: nil, limit: 25, page_offset: 0).and_return([@repo, @invalid_repo]) + it 'returns invalid repos' do + allow(client).to receive(:repos).with(filter: nil, limit: 25, page_offset: 0).and_return([@repo, @invalid_repo]) - get :status, format: :json + get :status, format: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['incompatible_repos'].length).to eq(1) - expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name) - expect(json_response['provider_repos'].length).to eq(1) - expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name) - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['incompatible_repos'].length).to eq(1) + expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name) + expect(json_response['provider_repos'].length).to eq(1) + expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name) end - it_behaves_like 'import controller with new_import_ui feature flag' do + it_behaves_like 'import controller status' do let(:repo) { @repo } let(:repo_id) { @repo.full_name } let(:import_source) { @repo.browse_url } @@ -177,47 +170,14 @@ RSpec.describe Import::BitbucketServerController do let(:client_repos_field) { :repos } end - it 'assigns repository categories' do - created_project = create(:project, :import_finished, import_type: 'bitbucket_server', creator_id: user.id, import_source: @created_repo.browse_url) - - expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]]) - expect(repos).to receive(:current_page).and_return(1) - expect(repos).to receive(:next_page).and_return(2) - expect(repos).to receive(:prev_page).and_return(nil) - expect(client).to receive(:repos).and_return(repos) - - get :status - - expect(assigns(:already_added_projects)).to eq([created_project]) - expect(assigns(:repos)).to eq([@repo]) - expect(assigns(:incompatible_repos)).to eq([@invalid_repo]) - end - context 'when filtering' do let(:filter) { 'test' } it 'passes filter param to bitbucket client' do - expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]]) - expect(client).to receive(:repos).with(filter: filter, limit: 25, page_offset: 0).and_return(repos) + expect(client).to receive(:repos).with(filter: filter, limit: 25, page_offset: 0).and_return([@repo]) get :status, params: { filter: filter }, as: :json end end end - - describe 'GET jobs' do - before do - assign_session_tokens - end - - it 'returns a list of imported projects' do - created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id) - - get :jobs - - expect(json_response.count).to eq(1) - expect(json_response.first['id']).to eq(created_project.id) - expect(json_response.first['import_status']).to eq('none') - end - end end diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb index aabbcb30358..376c089df78 100644 --- a/spec/controllers/import/fogbugz_controller_spec.rb +++ b/spec/controllers/import/fogbugz_controller_spec.rb @@ -82,36 +82,15 @@ RSpec.describe Import::FogbugzController do before do @repo = OpenStruct.new(id: 'demo', name: 'vim') stub_client(valid?: true) - stub_feature_flags(new_import_ui: false) end - it_behaves_like 'import controller with new_import_ui feature flag' do + it_behaves_like 'import controller status' do let(:repo) { @repo } let(:repo_id) { @repo.id } let(:import_source) { @repo.name } let(:provider_name) { 'fogbugz' } let(:client_repos_field) { :repos } end - - it 'assigns variables' do - @project = create(:project, import_type: 'fogbugz', creator_id: user.id) - stub_client(repos: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo]) - end - - it 'does not show already added project' do - @project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim') - stub_client(repos: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end end describe 'POST create' do diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 1cd0593f762..42c4348dac2 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -36,36 +36,15 @@ RSpec.describe Import::GitlabController do before do @repo = OpenStruct.new(id: 1, path: 'vim', path_with_namespace: 'asd/vim', web_url: 'https://gitlab.com/asd/vim') assign_session_token - stub_feature_flags(new_import_ui: false) end - it_behaves_like 'import controller with new_import_ui feature flag' do + it_behaves_like 'import controller status' do let(:repo) { @repo } let(:repo_id) { @repo.id } let(:import_source) { @repo.path_with_namespace } let(:provider_name) { 'gitlab' } let(:client_repos_field) { :projects } end - - it "assigns variables" do - @project = create(:project, import_type: 'gitlab', creator_id: user.id) - stub_client(projects: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo]) - end - - it "does not show already added project" do - @project = create(:project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim') - stub_client(projects: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end end describe "POST create" do diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index b3a83723189..9728fad417e 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -38,9 +38,9 @@ RSpec.describe Projects::NotesController do end it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do - last_fetched_at = 3.hours.ago.to_i + last_fetched_at = Time.zone.at(3.hours.ago.to_i) # remove nanoseconds - request.headers['X-Last-Fetched-At'] = last_fetched_at + request.headers['X-Last-Fetched-At'] = microseconds(last_fetched_at) expect(NotesFinder).to receive(:new) .with(anything, hash_including(last_fetched_at: last_fetched_at)) @@ -84,6 +84,81 @@ RSpec.describe Projects::NotesController do end end + context 'for multiple pages of notes', :aggregate_failures do + # 3 pages worth: 1 normal page, 1 oversized due to clashing updated_at, + # and a final, short page + let!(:page_1) { create_list(:note, 2, noteable: issue, project: project, updated_at: 3.days.ago) } + let!(:page_2) { create_list(:note, 3, noteable: issue, project: project, updated_at: 2.days.ago) } + let!(:page_3) { create_list(:note, 2, noteable: issue, project: project, updated_at: 1.day.ago) } + + # Include a resource event in the middle page as well + let!(:resource_event) { create(:resource_state_event, issue: issue, user: user, created_at: 2.days.ago) } + + let(:page_1_boundary) { microseconds(page_1.last.updated_at + NotesFinder::FETCH_OVERLAP) } + let(:page_2_boundary) { microseconds(page_2.last.updated_at + NotesFinder::FETCH_OVERLAP) } + + around do |example| + Timecop.freeze do + example.run + end + end + + before do + stub_const('Gitlab::UpdatedNotesPaginator::LIMIT', 2) + end + + context 'feature flag enabled' do + before do + stub_feature_flags(paginated_notes: true) + end + + it 'returns the first page of notes' do + get :index, params: request_params + + expect(json_response['notes'].count).to eq(page_1.count) + expect(json_response['more']).to be_truthy + expect(json_response['last_fetched_at']).to eq(page_1_boundary) + expect(response.headers['Poll-Interval'].to_i).to eq(1) + end + + it 'returns the second page of notes' do + request.headers['X-Last-Fetched-At'] = page_1_boundary + + get :index, params: request_params + + expect(json_response['notes'].count).to eq(page_2.count + 1) # resource event + expect(json_response['more']).to be_truthy + expect(json_response['last_fetched_at']).to eq(page_2_boundary) + expect(response.headers['Poll-Interval'].to_i).to eq(1) + end + + it 'returns the final page of notes' do + request.headers['X-Last-Fetched-At'] = page_2_boundary + + get :index, params: request_params + + expect(json_response['notes'].count).to eq(page_3.count) + expect(json_response['more']).to be_falsy + expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) + expect(response.headers['Poll-Interval'].to_i).to be > 1 + end + end + + context 'feature flag disabled' do + before do + stub_feature_flags(paginated_notes: false) + end + + it 'returns all notes' do + get :index, params: request_params + + expect(json_response['notes'].count).to eq((page_1 + page_2 + page_3).size + 1) + expect(json_response['more']).to be_falsy + expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) + end + end + end + context 'for a discussion note' do let(:project) { create(:project, :repository) } let!(:note) { create(:discussion_note_on_merge_request, project: project) } @@ -870,4 +945,9 @@ RSpec.describe Projects::NotesController do end end end + + # Convert a time to an integer number of microseconds + def microseconds(time) + (time.to_i * 1_000_000) + time.usec + end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 53d278f9cd0..e59493827ba 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -130,18 +130,6 @@ RSpec.describe ProjectsController do expect(json_response['count']).to eq(1) end - - context 'the feature flag is disabled' do - before do - stub_feature_flags(design_activity_events: false) - end - - it 'returns correct count' do - get_activity(project) - - expect(json_response['count']).to eq(0) - end - end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 5610f5889e6..868b126dc28 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -123,7 +123,7 @@ RSpec.describe NotesFinder do let!(:note1) { create :note_on_commit, project: project } let!(:note2) { create :note_on_commit, project: project } let(:commit) { note1.noteable } - let(:params) { { project: project, target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + let(:params) { { project: project, target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago } } it 'finds all notes' do notes = described_class.new(user, params).execute @@ -172,7 +172,7 @@ RSpec.describe NotesFinder do let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } - let(:params) { { project: confidential_issue.project, target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + let(:params) { { project: confidential_issue.project, target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago } } it 'returns notes if user can see the issue' do expect(described_class.new(user, params).execute).to eq([confidential_note]) @@ -204,7 +204,7 @@ RSpec.describe NotesFinder do end it 'returns the expected notes when last_fetched_at is given' do - params = { project: project, target: commit, last_fetched_at: 1.hour.ago.to_i } + params = { project: project, target: commit, last_fetched_at: 1.hour.ago } expect(described_class.new(user, params).execute).to eq([note2]) end diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb index 21a7d295dde..559d1004b4b 100644 --- a/spec/finders/user_recent_events_finder_spec.rb +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -37,28 +37,15 @@ RSpec.describe UserRecentEventsFinder do expect(finder.execute).to be_empty end - describe 'design_activity_events feature flag' do + describe 'design activity events' do let_it_be(:event_a) { create(:design_event, author: project_owner) } let_it_be(:event_b) { create(:design_event, author: project_owner) } - context 'the design_activity_events feature-flag is enabled' do - it 'only includes design events in enabled projects', :aggregate_failures do - events = finder.execute + it 'only includes design events', :aggregate_failures do + events = finder.execute - expect(events).to include(event_a) - expect(events).to include(event_b) - end - end - - context 'the design_activity_events feature-flag is disabled' do - it 'excludes design events', :aggregate_failures do - stub_feature_flags(design_activity_events: false) - - events = finder.execute - - expect(events).not_to include(event_a) - expect(events).not_to include(event_b) - end + expect(events).to include(event_a) + expect(events).to include(event_b) end end end diff --git a/spec/fixtures/clusters/ca_certificate.pem b/spec/fixtures/clusters/ca_certificate.pem new file mode 100644 index 00000000000..9e6810ab70c --- /dev/null +++ b/spec/fixtures/clusters/ca_certificate.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- diff --git a/spec/fixtures/clusters/chain_certificates.pem b/spec/fixtures/clusters/chain_certificates.pem new file mode 100644 index 00000000000..b8e64d58ee7 --- /dev/null +++ b/spec/fixtures/clusters/chain_certificates.pem @@ -0,0 +1,100 @@ +-----BEGIN CERTIFICATE----- +MIIItjCCB56gAwIBAgIQCu5Ga1hR41iahM0SWhyeNjANBgkqhkiG9w0BAQsFADB1 +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk +IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE5MTIwNDAwMDAwMFoXDTIxMTIwODEy +MDAwMFowgb0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB +BAGCNzwCAQMTAlVTMRUwEwYLKwYBBAGCNzwCAQITBFV0YWgxFTATBgNVBAUTDDUy +OTk1MzctMDE0MjELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxDTALBgNVBAcT +BExlaGkxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMRUwEwYDVQQDEwxkaWdpY2Vy +dC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAeRYb/RLbljGZ +IB//DrEdyKYMQqqaJwBlrr3t2paAWNuDJizvVkTMIzdJesI1pA58Myenxp5Dp8GJ +u/VhBf//v/HAZHUE4xwu104Fg6A1BwUEKgVKERf+7kTt17Lf9fcMIjMyL+FeyPXb +DOFbH+ej/nYaneFLch2j2xWZg1+Thk0qBlGE8WWAK+fvbEuM0SOeH9RkYFCNGPRS +KsLn0GvaCnnD4LfNDyMqYop0IpaqXoREEnkRv1MVSOw+hBj497wnnO+/GZegfzwU +iS60h+PjlDfmdCP18qOS7tRd0qnfU3N3S+PYEd3R63LMcIfbgXNEEWBNKpiH9+8f +eXq6bXKPAgMBAAGjggT3MIIE8zAfBgNVHSMEGDAWgBQ901Cl1qCt7vNKYApl0yHU ++PjWDzAdBgNVHQ4EFgQUTx0XO7HqD5DOhwlm2p+70uYPBmgwggGjBgNVHREEggGa +MIIBloIMZGlnaWNlcnQuY29tggl0aGF3dGUuZGWCC2ZyZWVzc2wuY29tggxyYXBp +ZHNzbC5jb22CDGdlb3RydXN0LmNvbYIJdGhhd3RlLmZyggp0aGF3dGUuY29tghB3 +d3cucmFwaWRzc2wuY29tghB3d3cuZ2VvdHJ1c3QuY29tgg13d3cudGhhd3RlLmZy +gg13d3cudGhhd3RlLmRlgg53d3cudGhhd3RlLmNvbYIQd3d3LmRpZ2ljZXJ0LmNv +bYIYa2ItaW50ZXJuYWwuZGlnaWNlcnQuY29tghprbm93bGVkZ2ViYXNlLmRpZ2lj +ZXJ0LmNvbYIWa25vd2xlZGdlLmRpZ2ljZXJ0LmNvbYIPa2guZGlnaWNlcnQuY29t +ghlrbm93bGVkZ2VodWIuZGlnaWNlcnQuY29tghh3ZWJzZWN1cml0eS5kaWdpY2Vy +dC5jb22CFGNvbnRlbnQuZGlnaWNlcnQuY29tgg93d3cuZnJlZXNzbC5jb22CHHd3 +dy53ZWJzZWN1cml0eS5kaWdpY2VydC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRw +Oi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3JsMDSgMqAw +hi5odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3Js +MEsGA1UdIAREMEIwNwYJYIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v +d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAk +BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAC +hkZodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5k +ZWRWYWxpZGF0aW9uU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF8BgorBgEE +AdZ5AgQCBIIBbASCAWgBZgB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7I +DdwQAAABbtLkOs4AAAQDAEYwRAIgQ7gh393PInhYfPOhg/lF9yZNRdvjBeufFoG8 +VnBuPNMCIBP8YGC83ig5ttw3ipSRjH0bKj4Ak5O4rynoql9Dy8x3AHYAVhQGmi/X +wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFu0uQ7VgAABAMARzBFAiEAhzE7 +1c48wn3s/30IB4WgxfpLburH0Ku8cchv8QeqcgACIBrWpUlDD18AOfkPCOcB2kWU +vRXsdptVm3jPeU5TtDSoAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e +0YUAAAFu0uQ60gAABAMARjBEAiBBpH5m7ntGKFTOFgSLcFXRDg66xJqerMy0gOHj +4TIBYAIgfFABPNy6P61hjiOWwjq73lvoEdAyh18GeFHIp0BgsWEwDQYJKoZIhvcN +AQELBQADggEBAInaSEqteyQA1zUKiXVqgffhHKZsUq9UnMows6X+UoFPoby9xqm6 +IaY/77zaFZYwXJlP/SvrlbgTLHAdir3y38uhAlfPX4iRuwggOpFFF5hqDckzCm91 +ocGnoG6sUY5mOqKu2vIcZkUQDe+K5gOxI6ME/4YwzWCIcTmBPQ6NQmqiFLPoQty1 +gdbGCcLQNFCuNq4n5OK2NmBjcbtyT4gglat7C4+KV8RkEubZ+MkXzyDkpEXjjzsK +7iuNB0hRgyyhGzHrlZ/l0OLoT0Cb4I5PzzRSseFEyPKCC1WSF7aE9rFfUqhpqSAT +7NV7SEijYyFFtuZfz9RGglcqnRlAfgTy+tU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW +YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY +uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/ +LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy +/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh +cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k +8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB +Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp +Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy +dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2 +MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j +b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW +gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh +hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg +4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa +2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs +1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1 +oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn +8TUoE6smftX3eg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- diff --git a/spec/fixtures/clusters/intermediate_certificate.pem b/spec/fixtures/clusters/intermediate_certificate.pem new file mode 100644 index 00000000000..8a81175b746 --- /dev/null +++ b/spec/fixtures/clusters/intermediate_certificate.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW +YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY +uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/ +LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy +/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh +cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k +8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB +Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp +Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy +dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2 +MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j +b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW +gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh +hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg +4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa +2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs +1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1 +oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn +8TUoE6smftX3eg== +-----END CERTIFICATE----- diff --git a/spec/fixtures/clusters/root_certificate.pem b/spec/fixtures/clusters/root_certificate.pem new file mode 100644 index 00000000000..40107bd837d --- /dev/null +++ b/spec/fixtures/clusters/root_certificate.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIItjCCB56gAwIBAgIQCu5Ga1hR41iahM0SWhyeNjANBgkqhkiG9w0BAQsFADB1 +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk +IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE5MTIwNDAwMDAwMFoXDTIxMTIwODEy +MDAwMFowgb0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB +BAGCNzwCAQMTAlVTMRUwEwYLKwYBBAGCNzwCAQITBFV0YWgxFTATBgNVBAUTDDUy +OTk1MzctMDE0MjELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxDTALBgNVBAcT +BExlaGkxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMRUwEwYDVQQDEwxkaWdpY2Vy +dC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAeRYb/RLbljGZ +IB//DrEdyKYMQqqaJwBlrr3t2paAWNuDJizvVkTMIzdJesI1pA58Myenxp5Dp8GJ +u/VhBf//v/HAZHUE4xwu104Fg6A1BwUEKgVKERf+7kTt17Lf9fcMIjMyL+FeyPXb +DOFbH+ej/nYaneFLch2j2xWZg1+Thk0qBlGE8WWAK+fvbEuM0SOeH9RkYFCNGPRS +KsLn0GvaCnnD4LfNDyMqYop0IpaqXoREEnkRv1MVSOw+hBj497wnnO+/GZegfzwU +iS60h+PjlDfmdCP18qOS7tRd0qnfU3N3S+PYEd3R63LMcIfbgXNEEWBNKpiH9+8f +eXq6bXKPAgMBAAGjggT3MIIE8zAfBgNVHSMEGDAWgBQ901Cl1qCt7vNKYApl0yHU ++PjWDzAdBgNVHQ4EFgQUTx0XO7HqD5DOhwlm2p+70uYPBmgwggGjBgNVHREEggGa +MIIBloIMZGlnaWNlcnQuY29tggl0aGF3dGUuZGWCC2ZyZWVzc2wuY29tggxyYXBp +ZHNzbC5jb22CDGdlb3RydXN0LmNvbYIJdGhhd3RlLmZyggp0aGF3dGUuY29tghB3 +d3cucmFwaWRzc2wuY29tghB3d3cuZ2VvdHJ1c3QuY29tgg13d3cudGhhd3RlLmZy +gg13d3cudGhhd3RlLmRlgg53d3cudGhhd3RlLmNvbYIQd3d3LmRpZ2ljZXJ0LmNv +bYIYa2ItaW50ZXJuYWwuZGlnaWNlcnQuY29tghprbm93bGVkZ2ViYXNlLmRpZ2lj +ZXJ0LmNvbYIWa25vd2xlZGdlLmRpZ2ljZXJ0LmNvbYIPa2guZGlnaWNlcnQuY29t +ghlrbm93bGVkZ2VodWIuZGlnaWNlcnQuY29tghh3ZWJzZWN1cml0eS5kaWdpY2Vy +dC5jb22CFGNvbnRlbnQuZGlnaWNlcnQuY29tgg93d3cuZnJlZXNzbC5jb22CHHd3 +dy53ZWJzZWN1cml0eS5kaWdpY2VydC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRw +Oi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3JsMDSgMqAw +hi5odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3Js +MEsGA1UdIAREMEIwNwYJYIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v +d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAk +BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAC +hkZodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5k +ZWRWYWxpZGF0aW9uU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF8BgorBgEE +AdZ5AgQCBIIBbASCAWgBZgB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7I +DdwQAAABbtLkOs4AAAQDAEYwRAIgQ7gh393PInhYfPOhg/lF9yZNRdvjBeufFoG8 +VnBuPNMCIBP8YGC83ig5ttw3ipSRjH0bKj4Ak5O4rynoql9Dy8x3AHYAVhQGmi/X +wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFu0uQ7VgAABAMARzBFAiEAhzE7 +1c48wn3s/30IB4WgxfpLburH0Ku8cchv8QeqcgACIBrWpUlDD18AOfkPCOcB2kWU +vRXsdptVm3jPeU5TtDSoAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e +0YUAAAFu0uQ60gAABAMARjBEAiBBpH5m7ntGKFTOFgSLcFXRDg66xJqerMy0gOHj +4TIBYAIgfFABPNy6P61hjiOWwjq73lvoEdAyh18GeFHIp0BgsWEwDQYJKoZIhvcN +AQELBQADggEBAInaSEqteyQA1zUKiXVqgffhHKZsUq9UnMows6X+UoFPoby9xqm6 +IaY/77zaFZYwXJlP/SvrlbgTLHAdir3y38uhAlfPX4iRuwggOpFFF5hqDckzCm91 +ocGnoG6sUY5mOqKu2vIcZkUQDe+K5gOxI6ME/4YwzWCIcTmBPQ6NQmqiFLPoQty1 +gdbGCcLQNFCuNq4n5OK2NmBjcbtyT4gglat7C4+KV8RkEubZ+MkXzyDkpEXjjzsK +7iuNB0hRgyyhGzHrlZ/l0OLoT0Cb4I5PzzRSseFEyPKCC1WSF7aE9rFfUqhpqSAT +7NV7SEijYyFFtuZfz9RGglcqnRlAfgTy+tU= +-----END CERTIFICATE----- diff --git a/spec/fixtures/product_analytics/event.json b/spec/fixtures/product_analytics/event.json new file mode 100644 index 00000000000..3100b068a0c --- /dev/null +++ b/spec/fixtures/product_analytics/event.json @@ -0,0 +1,16 @@ +{ + "aid":"1", + "p":"web", + "tna":"sp", + "tv":"js-2.14.0", + "eid":"fbf14096-74ee-47e4-883c-8a0d6cb72e37", + "duid":"79543c31-cfc3-4479-a737-fafb9333c8ba", + "sid":"54f6d3f3-f4f9-4fdc-87e0-a2c775234c1b", + "vid":4, + "url":"http://example.com/products/1", + "refr":"http://example.com/products/1", + "lang":"en-US", + "cookie":"1", + "tz":"America/Los_Angeles", + "cs":"UTF-8" +} diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js new file mode 100644 index 00000000000..fe08cf2c10a --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js @@ -0,0 +1,76 @@ +import { mount } from '@vue/test-utils'; +import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue'; +import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.graphql'; +import mockAlerts from '../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar To Do', () => { + let wrapper; + + function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { + wrapper = mount(SidebarTodo, { + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('updating the alert to do', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertTodo: { + errors: [], + alert: {}, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('renders a button for adding a To Do', () => { + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find('[data-testid="alert-todo-button"]').text()).toBe('Add a To Do'); + }); + }); + + it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + + return wrapper.vm.$nextTick().then(() => { + wrapper.find('button').trigger('click'); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: AlertMarkTodo, + variables: { + iid: '1527542', + projectPath: 'projectPath', + }, + }); + }); + }); + }); +}); diff --git a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb new file mode 100644 index 00000000000..11ee40a4c7e --- /dev/null +++ b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do + subject(:mutation) { described_class.new(object: project, context: { current_user: current_user }, field: nil) } + + let_it_be(:alert) { create(:alert_management_alert) } + let_it_be(:project) { alert.project } + let(:current_user) { project.owner } + + let(:args) { { project_path: project.full_path, iid: alert.iid } } + + specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) } + + describe '#resolve' do + subject(:resolve) { mutation.resolve(args) } + + context 'when user does not have permissions' do + let(:current_user) { nil } + + specify { expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) } + end + + context 'when project is invalid' do + let(:args) { { project_path: 'bunk/path', iid: alert.iid } } + + specify { expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) } + end + + context 'when alert is invalid' do + let(:args) { { project_path: project.full_path, iid: "-1" } } + + specify { expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) } + end + + context 'when the create service yields errors' do + let(:error_response) { double(error?: true, message: 'error', payload: { alert: {} }) } + + before do + allow_next_instance_of(::AlertManagement::Alerts::Todo::CreateService) do |service| + allow(service).to receive(:execute).and_return(error_response) + end + end + + specify { expect { resolve }.not_to change(Todo, :count) } + specify { expect(resolve[:errors]).to eq([error_response.message]) } + end + + context 'with valid inputs' do + it 'creates a new todo' do + expect { resolve }.to change { Todo.where(user: current_user, action: Todo::MARKED).count }.by(1) + end + + it { is_expected.to eq(alert: alert, todo: Todo.last, errors: []) } + end + end +end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index f0de602ed2d..4ca31405c1e 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -255,14 +255,6 @@ RSpec.describe EventsHelper do it { is_expected.to be(true) } end - context 'the feature flag is off' do - before do - stub_feature_flags(design_activity_events: false) - end - - it { is_expected.to be(false) } - end - context 'a project has been assigned' do before do assign(:project, project) @@ -277,14 +269,6 @@ RSpec.describe EventsHelper do it { is_expected.to be(false) } end - - context 'the feature flag is off' do - before do - stub_feature_flags(design_activity_events: false) - end - - it { is_expected.to be(false) } - end end context 'projects have been assigned' do @@ -309,14 +293,6 @@ RSpec.describe EventsHelper do it { is_expected.to be(false) } end - - context 'the feature flag is off' do - before do - stub_feature_flags(design_activity_events: false) - end - - it { is_expected.to be(false) } - end end context 'a group has been assigned' do @@ -344,14 +320,6 @@ RSpec.describe EventsHelper do it { is_expected.to be(false) } end - - context 'the feature flag is off' do - before do - stub_feature_flags(design_activity_events: false) - end - - it { is_expected.to be(false) } - end end end end diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index 0125f171ecb..bab48796b8c 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -88,16 +88,6 @@ RSpec.describe EventFilter do it 'returns only design events' do expect(filtered_events).to contain_exactly(design_event) end - - context 'the :design_activity_events feature is disabled' do - before do - stub_feature_flags(design_activity_events: false) - end - - it 'does not return design events' do - expect(filtered_events).to match_array(Event.not_design) - end - end end context 'with the "wiki" filter' do diff --git a/spec/lib/gitlab/updated_notes_paginator_spec.rb b/spec/lib/gitlab/updated_notes_paginator_spec.rb new file mode 100644 index 00000000000..eedc11777d4 --- /dev/null +++ b/spec/lib/gitlab/updated_notes_paginator_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UpdatedNotesPaginator do + let(:issue) { create(:issue) } + + let(:project) { issue.project } + let(:finder) { NotesFinder.new(user, target: issue, last_fetched_at: last_fetched_at) } + let(:user) { issue.author } + + let!(:page_1) { create_list(:note, 2, noteable: issue, project: project, updated_at: 2.days.ago) } + let!(:page_2) { [create(:note, noteable: issue, project: project, updated_at: 1.day.ago)] } + + let(:page_1_boundary) { page_1.last.updated_at + NotesFinder::FETCH_OVERLAP } + + around do |example| + Timecop.freeze do + example.run + end + end + + before do + stub_const("Gitlab::UpdatedNotesPaginator::LIMIT", 2) + end + + subject(:paginator) { described_class.new(finder.execute, last_fetched_at: last_fetched_at) } + + describe 'last_fetched_at: start of time' do + let(:last_fetched_at) { Time.at(0) } + + it 'calculates the first page of notes', :aggregate_failures do + expect(paginator.notes).to match_array(page_1) + expect(paginator.metadata).to match( + more: true, + last_fetched_at: microseconds(page_1_boundary) + ) + end + end + + describe 'last_fetched_at: start of final page' do + let(:last_fetched_at) { page_1_boundary } + + it 'calculates a final page', :aggregate_failures do + expect(paginator.notes).to match_array(page_2) + expect(paginator.metadata).to match( + more: false, + last_fetched_at: microseconds(Time.zone.now) + ) + end + end + + # Convert a time to an integer number of microseconds + def microseconds(time) + (time.to_i * 1_000_000) + time.usec + end +end diff --git a/spec/lib/product_analytics/event_params_spec.rb b/spec/lib/product_analytics/event_params_spec.rb new file mode 100644 index 00000000000..d6c098599d6 --- /dev/null +++ b/spec/lib/product_analytics/event_params_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ProductAnalytics::EventParams do + describe '.parse_event_params' do + subject { described_class.parse_event_params(raw_event) } + + let(:raw_event) { Gitlab::Json.parse(fixture_file('product_analytics/event.json')) } + + it 'extracts all params from raw event' do + expected_params = { + project_id: '1', + platform: 'web', + name_tracker: 'sp', + v_tracker: 'js-2.14.0', + event_id: 'fbf14096-74ee-47e4-883c-8a0d6cb72e37', + domain_userid: '79543c31-cfc3-4479-a737-fafb9333c8ba', + domain_sessionid: '54f6d3f3-f4f9-4fdc-87e0-a2c775234c1b', + domain_sessionidx: 4, + page_url: 'http://example.com/products/1', + page_referrer: 'http://example.com/products/1', + br_lang: 'en-US', + br_cookies: true, + os_timezone: 'America/Los_Angeles', + doc_charset: 'UTF-8' + } + + expect(subject).to include(expected_params) + end + end + + describe '.has_required_params?' do + subject { described_class.has_required_params?(params) } + + context 'aid and eid are present' do + let(:params) { { 'aid' => 1, 'eid' => 2 } } + + it { expect(subject).to be_truthy } + end + + context 'aid and eid are missing' do + let(:params) { {} } + + it { expect(subject).to be_falsey } + end + + context 'eid is missing' do + let(:params) { { 'aid' => 1 } } + + it { expect(subject).to be_falsey } + end + end +end diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb index fb523092f7a..477fb16400a 100644 --- a/spec/mailers/emails/merge_requests_spec.rb +++ b/spec/mailers/emails/merge_requests_spec.rb @@ -17,4 +17,20 @@ RSpec.describe Emails::MergeRequests do expect(subject).to have_body_text current_user.name end end + + describe "#merge_when_pipeline_succeeds_email" do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:current_user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:title) { "Merge request #{merge_request.to_reference} was scheduled to merge after pipeline succeeds by #{current_user.name}" } + + subject { Notify.merge_when_pipeline_succeeds_email(user.id, merge_request.id, current_user.id) } + + it "has required details" do + expect(subject).to have_content title + expect(subject).to have_content merge_request.to_reference + expect(subject).to have_content current_user.name + end + end end diff --git a/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb new file mode 100644 index 00000000000..81fa29f4c54 --- /dev/null +++ b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200623142159_remove_gitlab_issue_tracker_service_records.rb') + +RSpec.describe RemoveGitlabIssueTrackerServiceRecords do + let(:services) { table(:services) } + + before do + 5.times { services.create!(type: 'GitlabIssueTrackerService') } + services.create!(type: 'SomeOtherType') + end + + it 'removes services records of type GitlabIssueTrackerService', :aggregate_failures do + expect { migrate! }.to change { services.count }.from(6).to(1) + expect(services.first.type).to eq('SomeOtherType') + expect(services.where(type: 'GitlabIssueTrackerService')).to be_empty + end +end diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index 06ecd674928..cc314d9077d 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -58,4 +58,11 @@ RSpec.describe ApplicationRecord do expect(MergeRequest.underscore).to eq('merge_request') end end + + describe '.at_most' do + it 'limits the number of records returned' do + create_list(:user, 3) + expect(User.at_most(2).count).to eq(2) + end + end end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index de5cd01df55..adccc72d13d 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -204,6 +204,52 @@ RSpec.describe Clusters::Platforms::Kubernetes do end it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) } + + context 'ca_pem is a single certificate' do + let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/ca_certificate.pem')) } + let(:kubernetes) do + build(:cluster_platform_kubernetes, + :configured, + namespace: 'a-namespace', + cluster: cluster, + ca_pem: ca_pem) + end + + it 'adds it to cert_store' do + cert = OpenSSL::X509::Certificate.new(ca_pem) + cert_store = kubernetes.kubeclient.kubeclient_options[:ssl_options][:cert_store] + + expect(cert_store.verify(cert)).to be true + end + end + + context 'ca_pem is a chain' do + let(:cert_chain) { File.read(Rails.root.join('spec/fixtures/clusters/chain_certificates.pem')) } + let(:kubernetes) do + build(:cluster_platform_kubernetes, + :configured, + namespace: 'a-namespace', + cluster: cluster, + ca_pem: cert_chain) + end + + it 'includes chain of certificates' do + cert1_file = File.read(Rails.root.join('spec/fixtures/clusters/root_certificate.pem')) + cert1 = OpenSSL::X509::Certificate.new(cert1_file) + + cert2_file = File.read(Rails.root.join('spec/fixtures/clusters/intermediate_certificate.pem')) + cert2 = OpenSSL::X509::Certificate.new(cert2_file) + + cert3_file = File.read(Rails.root.join('spec/fixtures/clusters/ca_certificate.pem')) + cert3 = OpenSSL::X509::Certificate.new(cert3_file) + + cert_store = kubernetes.kubeclient.kubeclient_options[:ssl_options][:cert_store] + + expect(cert_store.verify(cert1)).to be true + expect(cert_store.verify(cert2)).to be true + expect(cert_store.verify(cert3)).to be true + end + end end describe '#rbac?' do diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb index a1773378073..aca2a8c3a2f 100644 --- a/spec/models/event_collection_spec.rb +++ b/spec/models/event_collection_spec.rb @@ -50,24 +50,6 @@ RSpec.describe EventCollection do expect(events).to include(wiki_page_event) end - context 'the design_activity_events feature flag is disabled' do - before do - stub_feature_flags(design_activity_events: false) - end - - it 'omits the design events when using to_a' do - events = described_class.new(projects).to_a - - expect(events).not_to include(design_event) - end - - it 'omits the wiki page events when using all_project_events' do - events = described_class.new(projects).all_project_events - - expect(events).not_to include(design_event) - end - end - it 'includes the design events' do collection = described_class.new(projects) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index a05ae188ef6..96baeab6809 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -643,15 +643,6 @@ RSpec.describe Event do end end - describe '.not_design' do - it 'does not contain the design events' do - non_design_events = events.reject(&:design?) - - expect(events).not_to match_array(non_design_events) - expect(described_class.not_design).to match_array(non_design_events) - end - end - describe '.for_wiki_page' do it 'only contains the wiki page events' do wiki_events = events.select(&:wiki_page?) diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb index 6593edae8ac..6058df9fa13 100644 --- a/spec/models/product_analytics_event_spec.rb +++ b/spec/models/product_analytics_event_spec.rb @@ -5,6 +5,13 @@ RSpec.describe ProductAnalyticsEvent, type: :model do it { is_expected.to belong_to(:project) } it { expect(described_class).to respond_to(:order_by_time) } + describe 'validations' do + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:event_id) } + it { is_expected.to validate_presence_of(:v_collector) } + it { is_expected.to validate_presence_of(:v_etl) } + end + describe '.timerange' do let_it_be(:event_1) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 1.day) } let_it_be(:event_2) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 5.days) } diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb new file mode 100644 index 00000000000..b68541b5d92 --- /dev/null +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -0,0 +1,461 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::API::Admin::InstanceClusters do + include KubernetesHelpers + + let_it_be(:regular_user) { create(:user) } + let_it_be(:admin_user) { create(:admin) } + let_it_be(:project) { create(:project) } + let_it_be(:project_cluster) do + create(:cluster, :project, :provided_by_gcp, + user: admin_user, + projects: [project]) + end + let(:project_cluster_id) { project_cluster.id } + + describe "GET /admin/clusters" do + let_it_be(:clusters) do + create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment) + end + + context "when authenticated as a non-admin user" do + it 'returns 403' do + get api('/admin/clusters', regular_user) + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when authenticated as admin" do + before do + get api("/admin/clusters", admin_user) + end + + it 'returns 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'includes pagination headers' do + expect(response).to include_pagination_headers + end + + it 'only returns the instance clusters' do + cluster_ids = json_response.map { |cluster| cluster['id'] } + expect(cluster_ids).to match_array(clusters.pluck(:id)) + expect(cluster_ids).not_to include(project_cluster_id) + end + end + end + + describe "GET /admin/clusters/:cluster_id" do + let_it_be(:platform_kubernetes) do + create(:cluster_platform_kubernetes, :configured) + end + + let_it_be(:cluster) do + create(:cluster, :instance, :provided_by_gcp, :with_domain, + platform_kubernetes: platform_kubernetes, + user: admin_user) + end + + let(:cluster_id) { cluster.id } + + context "when authenticated as admin" do + before do + get api("/admin/clusters/#{cluster_id}", admin_user) + end + + context "when no cluster associated to the ID" do + let(:cluster_id) { 1337 } + + it 'returns 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when cluster with cluster_id exists" do + it 'returns 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns the cluster with cluster_id' do + expect(json_response['id']).to eq(cluster.id) + end + + it 'returns the cluster information' do + expect(json_response['provider_type']).to eq('gcp') + expect(json_response['platform_type']).to eq('kubernetes') + expect(json_response['environment_scope']).to eq('*') + expect(json_response['cluster_type']).to eq('instance_type') + expect(json_response['domain']).to eq('example.com') + end + + it 'returns kubernetes platform information' do + platform = json_response['platform_kubernetes'] + + expect(platform['api_url']).to eq('https://kubernetes.example.com') + expect(platform['ca_cert']).to be_present + end + + it 'returns user information' do + user = json_response['user'] + + expect(user['id']).to eq(admin_user.id) + expect(user['username']).to eq(admin_user.username) + end + + it 'returns GCP provider information' do + gcp_provider = json_response['provider_gcp'] + + expect(gcp_provider['cluster_id']).to eq(cluster.id) + expect(gcp_provider['status_name']).to eq('created') + expect(gcp_provider['gcp_project_id']).to eq('test-gcp-project') + expect(gcp_provider['zone']).to eq('us-central1-a') + expect(gcp_provider['machine_type']).to eq('n1-standard-2') + expect(gcp_provider['num_nodes']).to eq(3) + expect(gcp_provider['endpoint']).to eq('111.111.111.111') + end + + context 'when cluster has no provider' do + let(:cluster) do + create(:cluster, :instance, :provided_by_user, :production_environment) + end + + it 'does not include GCP provider info' do + expect(json_response['provider_gcp']).not_to be_present + end + end + + context 'when trying to get a project cluster via the instance cluster endpoint' do + it 'returns 404' do + get api("/admin/clusters/#{project_cluster_id}", admin_user) + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context "when authenticated as a non-admin user" do + it 'returns 403' do + get api("/admin/clusters/#{cluster_id}", regular_user) + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + end + + describe "POST /admin/clusters/add" do + let(:api_url) { 'https://example.com' } + let(:authorization_type) { 'rbac' } + let(:clusterable) { Clusters::Instance.new } + + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'sample-token', + authorization_type: authorization_type + } + end + + let(:cluster_params) do + { + name: 'test-instance-cluster', + domain: 'domain.example.com', + managed: false, + platform_kubernetes_attributes: platform_kubernetes_attributes, + clusterable: clusterable + } + end + + let(:multiple_cluster_params) do + { + name: 'multiple-instance-cluster', + environment_scope: 'staging/*', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + let(:invalid_cluster_params) do + { + environment_scope: 'production/*', + domain: 'domain.example.com', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + context 'authorized user' do + before do + post api('/admin/clusters/add', admin_user), params: cluster_params + end + + context 'with valid params' do + it 'responds with 201' do + expect(response).to have_gitlab_http_status(:created) + end + + it 'creates a new Clusters::Cluster', :aggregate_failures do + cluster_result = Clusters::Cluster.find(json_response["id"]) + platform_kubernetes = cluster_result.platform + expect(cluster_result).to be_user + expect(cluster_result).to be_kubernetes + expect(cluster_result.clusterable).to be_a Clusters::Instance + expect(cluster_result.cluster_type).to eq('instance_type') + expect(cluster_result.name).to eq('test-instance-cluster') + expect(cluster_result.domain).to eq('domain.example.com') + expect(cluster_result.environment_scope).to eq('*') + expect(cluster_result.enabled).to eq(true) + expect(platform_kubernetes.authorization_type).to eq('rbac') + expect(cluster_result.managed).to be_falsy + expect(platform_kubernetes.api_url).to eq("https://example.com") + expect(platform_kubernetes.token).to eq('sample-token') + end + + context 'when user does not indicate authorization type' do + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'sample-token' + } + end + + it 'defaults to RBAC' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result.platform_kubernetes.rbac?).to be_truthy + end + end + + context 'when user sets authorization type as ABAC' do + let(:authorization_type) { 'abac' } + + it 'creates an ABAC cluster' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result.platform.abac?).to be_truthy + end + end + + context 'when an instance cluster already exists' do + it 'allows user to add multiple clusters' do + post api('/admin/clusters/add', admin_user), params: multiple_cluster_params + + expect(Clusters::Instance.new.clusters.count).to eq(2) + end + end + end + + context 'with invalid params' do + context 'when missing a required parameter' do + it 'responds with 400' do + post api('/admin/clusters/add', admin_user), params: invalid_cluster_params + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eql('name is missing') + end + end + + context 'with a malformed api url' do + let(:api_url) { 'invalid_api_url' } + + it 'responds with 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns validation errors' do + expect(json_response['message']['platform_kubernetes.api_url'].first).to be_present + end + end + end + end + + context 'non-authorized user' do + it 'responds with 403' do + post api('/admin/clusters/add', regular_user), params: cluster_params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'PUT /admin/clusters/:cluster_id' do + let(:api_url) { 'https://example.com' } + + let(:update_params) do + { + domain: domain, + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + let(:domain) { 'new-domain.com' } + let(:platform_kubernetes_attributes) { {} } + + let_it_be(:cluster) do + create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com') + end + + context 'authorized user' do + before do + put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params + + cluster.reload + end + + context 'with valid params' do + it 'responds with 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'updates cluster attributes' do + expect(cluster.domain).to eq('new-domain.com') + end + end + + context 'with invalid params' do + let(:domain) { 'invalid domain' } + + it 'responds with 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'does not update cluster attributes' do + expect(cluster.domain).to eq('old-domain.com') + end + + it 'returns validation errors' do + expect(json_response['message']['domain'].first).to match('contains invalid characters (valid characters: [a-z0-9\\-])') + end + end + + context 'with a GCP cluster' do + context 'when user tries to change GCP specific fields' do + let(:platform_kubernetes_attributes) do + { + api_url: 'https://new-api-url.com', + token: 'new-sample-token' + } + end + + it 'responds with 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns validation error' do + expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster')) + end + end + + context 'when user tries to change domain' do + let(:domain) { 'new-domain.com' } + + it 'responds with 200' do + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with an user cluster' do + let(:api_url) { 'https://new-api-url.com' } + + let(:cluster) do + create(:cluster, :instance, :provided_by_user, :production_environment) + end + + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'new-sample-token' + } + end + + let(:update_params) do + { + name: 'new-name', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + it 'responds with 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'updates platform kubernetes attributes' do + platform_kubernetes = cluster.platform_kubernetes + + expect(cluster.name).to eq('new-name') + expect(platform_kubernetes.api_url).to eq('https://new-api-url.com') + expect(platform_kubernetes.token).to eq('new-sample-token') + end + end + + context 'with a cluster that does not exist' do + let(:cluster_id) { 1337 } + + it 'returns 404' do + put api("/admin/clusters/#{cluster_id}", admin_user), params: update_params + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when trying to update a project cluster via the instance cluster endpoint' do + it 'returns 404' do + put api("/admin/clusters/#{project_cluster_id}", admin_user), params: update_params + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'non-authorized user' do + it 'responds with 403' do + put api("/admin/clusters/#{cluster.id}", regular_user), params: update_params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe 'DELETE /admin/clusters/:cluster_id' do + let(:cluster_params) { { cluster_id: cluster.id } } + + let_it_be(:cluster) do + create(:cluster, :instance, :provided_by_gcp) + end + + context 'authorized user' do + before do + delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params + end + + it 'responds with 204' do + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'deletes the cluster' do + expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy + end + + context 'with a cluster that does not exist' do + let(:cluster_id) { 1337 } + + it 'returns 404' do + delete api("/admin/clusters/#{cluster_id}", admin_user) + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when trying to update a project cluster via the instance cluster endpoint' do + it 'returns 404' do + delete api("/admin/clusters/#{project_cluster_id}", admin_user) + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'non-authorized user' do + it 'responds with 403' do + delete api("/admin/clusters/#{cluster.id}", regular_user), params: cluster_params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb new file mode 100644 index 00000000000..e5803f50474 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creating a todo for the alert' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:alert) { create(:alert_management_alert, project: project) } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: alert.iid.to_s + } + graphql_mutation(:alert_todo_create, variables) do + <<~QL + clientMutationId + errors + todo { + author { + username + } + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:alert_todo_create) } + + before do + project.add_developer(user) + end + + it 'creates a todo for the current user' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['todo']['author']['username']).to eq(user.username) + end + + context 'todo already exists' do + before do + create(:todo, :pending, project: project, user: user, target: alert) + end + + it 'surfaces an error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to eq(['You already have pending todo for this alert']) + end + end +end diff --git a/spec/requests/product_analytics/collector_app_attack_spec.rb b/spec/requests/product_analytics/collector_app_attack_spec.rb new file mode 100644 index 00000000000..6f86e39c295 --- /dev/null +++ b/spec/requests/product_analytics/collector_app_attack_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ProductAnalytics::CollectorApp throttle' do + include RackAttackSpecHelpers + + include_context 'rack attack cache store' + + let(:project1) { create(:project) } + let(:project2) { create(:project) } + + before do + allow(ProductAnalyticsEvent).to receive(:create).and_return(true) + end + + context 'per application id' do + let(:params) do + { + aid: project1.id, + eid: SecureRandom.uuid + } + end + + it 'throttles the endpoint' do + # Allow requests under the rate limit. + 100.times do + expect_ok { get '/-/collector/i', params: params } + end + + # Ensure its not related to ip address + random_next_ip + + # Reject request over the limit + expect_rejection { get '/-/collector/i', params: params } + + # But allows request for different aid + expect_ok { get '/-/collector/i', params: params.merge(aid: project2.id) } + end + end +end diff --git a/spec/requests/product_analytics/collector_app_spec.rb b/spec/requests/product_analytics/collector_app_spec.rb new file mode 100644 index 00000000000..0491c2564f0 --- /dev/null +++ b/spec/requests/product_analytics/collector_app_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ProductAnalytics::CollectorApp' do + let_it_be(:project) { create(:project) } + let(:params) { {} } + + subject { get '/-/collector/i', params: params } + + RSpec.shared_examples 'not found' do + it 'repond with 404' do + expect { subject }.not_to change { ProductAnalyticsEvent.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'correct event params' do + let(:params) do + { + aid: project.id, + p: 'web', + tna: 'sp', + tv: 'js-2.14.0', + eid: SecureRandom.uuid, + duid: SecureRandom.uuid, + sid: SecureRandom.uuid, + vid: 4, + url: 'http://example.com/products/1', + refr: 'http://example.com/products/1', + lang: 'en-US', + cookie: true, + tz: 'America/Los_Angeles', + cs: 'UTF-8' + } + end + + it 'repond with 200' do + expect { subject }.to change { ProductAnalyticsEvent.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'feature disabled' do + before do + stub_feature_flags(product_analytics: false) + end + + it_behaves_like 'not found' + end + end + + context 'empty event params' do + it_behaves_like 'not found' + end + + context 'invalid project id in params' do + let(:params) do + { + aid: '-1', + p: 'web', + tna: 'sp', + tv: 'js-2.14.0', + eid: SecureRandom.uuid, + duid: SecureRandom.uuid, + sid: SecureRandom.uuid + } + end + + it_behaves_like 'not found' + end +end diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb index 0ec418d33d1..15d2f32de78 100644 --- a/spec/routing/import_routing_spec.rb +++ b/spec/routing/import_routing_spec.rb @@ -90,25 +90,39 @@ RSpec.describe Import::GiteaController, 'routing' do end end -# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status -# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback -# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs -# import_gitlab POST /import/gitlab(.:format) import/gitlab#create +# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status +# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback +# realtime_changes_import_gitlab GET /import/gitlab/realtime_changes(.:format) import/gitlab#realtime_changes +# import_gitlab POST /import/gitlab(.:format) import/gitlab#create RSpec.describe Import::GitlabController, 'routing' do it_behaves_like 'importer routing' do let(:except_actions) { [:new] } let(:provider) { 'gitlab' } + let(:is_realtime) { true } end end -# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status -# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback -# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs -# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create +# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status +# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback +# realtime_changes_import_bitbucket GET /import/bitbucket/realtime_changes(.:format) import/bitbucket#realtime_changes +# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create RSpec.describe Import::BitbucketController, 'routing' do it_behaves_like 'importer routing' do let(:except_actions) { [:new] } let(:provider) { 'bitbucket' } + let(:is_realtime) { true } + end +end + +# status_import_bitbucket_server GET /import/bitbucket_server/status(.:format) import/bitbucket_server#status +# callback_import_bitbucket_server GET /import/bitbucket_server/callback(.:format) import/bitbucket_server#callback +# realtime_changes_import_bitbucket_server GET /import/bitbucket_server/realtime_changes(.:format) import/bitbucket_server#realtime_changes +# new_import_bitbucket_server GET /import/bitbucket_server/new(.:format) import/bitbucket_server#new +# import_bitbucket_server POST /import/bitbucket_server(.:format) import/bitbucket_server#create +RSpec.describe Import::BitbucketServerController, 'routing' do + it_behaves_like 'importer routing' do + let(:provider) { 'bitbucket_server' } + let(:is_realtime) { true } end end @@ -138,17 +152,18 @@ RSpec.describe Import::GoogleCodeController, 'routing' do end end -# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status -# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback -# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs -# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map -# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map -# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create -# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new +# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status +# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback +# realtime_changes_import_fogbugz GET /import/fogbugz/realtime_changes(.:format) import/fogbugz#realtime_changes +# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map +# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map +# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create +# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new RSpec.describe Import::FogbugzController, 'routing' do it_behaves_like 'importer routing' do let(:except_actions) { [:callback] } let(:provider) { 'fogbugz' } + let(:is_realtime) { true } end it 'to #callback' do diff --git a/spec/services/alert_management/alerts/todo/create_service_spec.rb b/spec/services/alert_management/alerts/todo/create_service_spec.rb new file mode 100644 index 00000000000..e3d9de8b4df --- /dev/null +++ b/spec/services/alert_management/alerts/todo/create_service_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AlertManagement::Alerts::Todo::CreateService do + let_it_be(:user) { create(:user) } + let_it_be(:alert) { create(:alert_management_alert) } + + let(:current_user) { user } + + describe '#execute' do + subject(:result) { AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute } + + shared_examples 'permissions error' do + it 'returns an error', :aggregate_failures do + expect(result.error?).to be(true) + expect(result.message).to eq('You have insufficient permissions to create a Todo for this alert') + expect(result.payload[:todo]).to be(nil) + expect(result.payload[:alert]).to be(alert) + end + end + + context 'when the user is anonymous' do + let(:current_user) { nil } + + it_behaves_like 'permissions error' + end + + context 'when the user does not have permission' do + it_behaves_like 'permissions error' + end + + context 'when user has permission' do + before do + alert.project.add_developer(user) + end + + it 'creates a todo' do + expect { result }.to change { Todo.count }.by(1) + end + + it 'returns the alert and todo in the payload', :aggregate_failures do + expect(result.success?).to be(true) + expect(result.payload[:alert][:id]).to be(alert.id) + expect(result.payload[:todo][:id]).to be(Todo.last.id) + end + + context 'when the user has a marked todo for the alert' do + let_it_be(:todo_params) do + { project: alert.project, + target: alert, + user: user, + action: Todo::MARKED } + end + + context 'when todo is pending' do + before_all do + create(:todo, :pending, **todo_params) + end + + it 'does not create a todo' do + expect { result }.not_to change { Todo.count } + end + + it 'returns an error', :aggregate_failures do + expect(result.error?).to be(true) + expect(result.message).to be('You already have pending todo for this alert') + expect(result.payload[:todo]).to be(nil) + expect(result.payload[:alert]).to be(alert) + end + end + + context 'when todo is done' do + before do + create(:todo, :done, **todo_params) + end + + it { expect(result.success?).to be(true) } + it { expect { result }.to change { Todo.count }.by(1) } + end + end + end + end +end diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb index 092742276d3..3bf59f6a2d1 100644 --- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb @@ -69,6 +69,7 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do before do allow(merge_request) .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline) + expect(MailScheduler::NotificationServiceWorker).to receive(:perform_async).with('merge_when_pipeline_succeeds', merge_request, user).once service.execute(merge_request) end @@ -90,6 +91,18 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do end end + context 'without feature enabled' do + it 'does not send notification' do + stub_feature_flags(mwps_notification: false) + + allow(merge_request) + .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline) + expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async) + + service.execute(merge_request) + end + end + context 'already approved' do let(:service) { described_class.new(project, user, should_remove_source_branch: true) } let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } @@ -106,6 +119,7 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do it 'updates the merge params' do expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds) + expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async).with('merge_when_pipeline_succeeds', any_args) service.execute(mr_merge_if_green_enabled) expect(mr_merge_if_green_enabled.merge_params).to have_key('should_remove_source_branch') diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 7e4b61ed1b7..d10ed7d6640 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -290,18 +290,6 @@ RSpec.describe EventCreateService do let_it_be(:design) { create(:design, project: project) } let_it_be(:author) { user } - shared_examples 'feature flag gated multiple event creation' do - context 'the feature flag is off' do - before do - stub_feature_flags(design_activity_events: false) - end - - specify { expect(result).to be_empty } - specify { expect { result }.not_to change { Event.count } } - specify { expect { result }.not_to exceed_query_limit(0) } - end - end - describe '#save_designs' do let_it_be(:updated) { create_list(:design, 5) } let_it_be(:created) { create_list(:design, 3) } @@ -326,8 +314,6 @@ RSpec.describe EventCreateService do expect(events.map(&:design)).to match_array(updated) end - it_behaves_like 'feature flag gated multiple event creation' - it 'records the event in the event counter' do stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions @@ -356,8 +342,6 @@ RSpec.describe EventCreateService do expect(events.map(&:design)).to match_array(designs) end - it_behaves_like 'feature flag gated multiple event creation' - it 'records the event in the event counter' do stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 9c837019d37..2fe7a46de4b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -2023,6 +2023,26 @@ RSpec.describe NotificationService, :mailer do let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) } end end + + describe '#merge_when_pipeline_succeeds' do + it 'send notification that merge will happen when pipeline succeeds' do + notification.merge_when_pipeline_succeeds(merge_request, assignee) + should_email(merge_request.author) + should_email(@u_watcher) + should_email(@subscriber) + end + + it_behaves_like 'participating notifications' do + let(:participant) { create(:user, username: 'user-participant') } + let(:issuable) { merge_request } + let(:notification_trigger) { notification.merge_when_pipeline_succeeds(merge_request, @u_disabled) } + end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.merge_when_pipeline_succeeds(merge_request, @u_disabled) } + end + end end describe 'Projects', :deliver_mails_inline do diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb index f9ddf954cd5..6209294f4ce 100644 --- a/spec/services/resource_events/merge_into_notes_service_spec.rb +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -61,7 +61,7 @@ RSpec.describe ResourceEvents::MergeIntoNotesService do event = create_event(created_at: 1.day.ago) notes = described_class.new(resource, user, - last_fetched_at: 2.days.ago.to_i).execute + last_fetched_at: 2.days.ago).execute expect(notes.count).to eq 1 expect(notes.first.discussion_id).to eq event.discussion_id diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb index e0cedb5a57b..65082ec690f 100644 --- a/spec/support/helpers/rack_attack_spec_helpers.rb +++ b/spec/support/helpers/rack_attack_spec_helpers.rb @@ -30,4 +30,16 @@ module RackAttackSpecHelpers expect(response).to have_gitlab_http_status(:too_many_requests) end + + def expect_ok(&block) + yield + + expect(response).to have_gitlab_http_status(:ok) + end + + def random_next_ip + allow_next_instance_of(Rack::Attack::Request) do |instance| + allow(instance).to receive(:ip).and_return(FFaker::Internet.ip_v4_address) + end + end end diff --git a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb deleted file mode 100644 index 88ad1f6cde2..00000000000 --- a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'import controller with new_import_ui feature flag' do - include ImportSpecHelper - - context 'with new_import_ui feature flag enabled' do - let(:group) { create(:group) } - - before do - stub_feature_flags(new_import_ui: true) - group.add_owner(user) - end - - it "returns variables for json request" do - project = create(:project, import_type: provider_name, creator_id: user.id) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) - end - - it "does not show already added project" do - project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos")).to eq([]) - end - end -end diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb new file mode 100644 index 00000000000..ecb9abc5c46 --- /dev/null +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'import controller status' do + include ImportSpecHelper + + let(:group) { create(:group) } + + before do + group.add_owner(user) + end + + it "returns variables for json request" do + project = create(:project, import_type: provider_name, creator_id: user.id) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end + + it "does not show already added project" do + project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end +end |