Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-17 03:09:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-17 03:09:37 +0300
commitd5cf5cf4f77eec07a04604b1a0298452029df16f (patch)
tree7fafba2450f0cc0160fbacfbd94a0b11ab47dc12
parent831b6108d2aa46aca9bdce39a9bda33718d61fa7 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/alert_management/components/alert_sidebar.vue11
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue27
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue106
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql11
-rw-r--r--app/assets/javascripts/header.js5
-rw-r--r--app/controllers/concerns/notes_actions.rb82
-rw-r--r--app/controllers/import/base_controller.rb11
-rw-r--r--app/controllers/import/bitbucket_controller.rb17
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb17
-rw-r--r--app/controllers/import/fogbugz_controller.rb18
-rw-r--r--app/controllers/import/gitea_controller.rb14
-rw-r--r--app/controllers/import/github_controller.rb99
-rw-r--r--app/controllers/import/gitlab_controller.rb20
-rw-r--r--app/finders/notes_finder.rb9
-rw-r--r--app/finders/user_recent_events_finder.rb9
-rw-r--r--app/graphql/mutations/alert_management/alerts/todo/create.rb30
-rw-r--r--app/graphql/mutations/alert_management/base.rb5
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/todo_target_enum.rb1
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb7
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/application_record.rb4
-rw-r--r--app/models/clusters/platforms/kubernetes.rb11
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/event_collection.rb9
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/product_analytics_event.rb2
-rw-r--r--app/models/resource_event.rb1
-rw-r--r--app/services/alert_management/alerts/todo/create_service.rb51
-rw-r--r--app/services/auto_merge/base_service.rb6
-rw-r--r--app/services/auto_merge/merge_when_pipeline_succeeds_service.rb8
-rw-r--r--app/services/event_create_service.rb3
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb20
-rw-r--r--app/services/resource_events/synthetic_label_notes_builder_service.rb2
-rw-r--r--app/services/resource_events/synthetic_milestone_notes_builder_service.rb2
-rw-r--r--app/services/resource_events/synthetic_state_notes_builder_service.rb2
-rw-r--r--app/services/todo_service.rb6
-rw-r--r--app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json5
-rw-r--r--app/views/import/bitbucket/status.html.haml91
-rw-r--r--app/views/import/bitbucket_server/status.html.haml92
-rw-r--r--app/views/import/fogbugz/status.html.haml65
-rw-r--r--app/views/import/gitlab/status.html.haml53
-rw-r--r--app/views/notify/merge_when_pipeline_succeeds_email.html.haml161
-rw-r--r--app/views/notify/merge_when_pipeline_succeeds_email.text.haml8
-rw-r--r--changelogs/unreleased/215946-add-gitlab-to-do-for-user-when-they-are-assigned-to-an-alert-2.yml5
-rw-r--r--changelogs/unreleased/217673-keyset-paginate-notes-backend-only.yml5
-rw-r--r--changelogs/unreleased/218526_backstage_remove_gitlab_issue_tracker_service_records.yml5
-rw-r--r--changelogs/unreleased/31000-api-for-instance-level-kubernetes-clusters.yml5
-rw-r--r--changelogs/unreleased/ajk-remove-design-events-ff.yml5
-rw-r--r--changelogs/unreleased/merge-tslint-with-eslint.yml5
-rw-r--r--changelogs/unreleased/patch-109.yml5
-rw-r--r--config/initializers/rack_attack.rb13
-rw-r--r--config/routes.rb4
-rw-r--r--config/routes/import.rb4
-rw-r--r--db/post_migrate/20200623142159_remove_gitlab_issue_tracker_service_records.rb28
-rw-r--r--db/structure.sql1
-rw-r--r--doc/administration/gitaly/praefect.md2
-rw-r--r--doc/api/api_resources.md1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql71
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json219
-rw-r--r--doc/api/graphql/reference/index.md15
-rw-r--r--doc/api/instance_clusters.md293
-rw-r--r--doc/ci/runners/README.md31
-rw-r--r--doc/development/database_review.md4
-rw-r--r--doc/development/gitaly.md13
-rw-r--r--doc/install/aws/index.md3
-rw-r--r--doc/user/application_security/configuration/index.md2
-rw-r--r--doc/user/application_security/sast/analyzers.md37
-rw-r--r--doc/user/application_security/sast/index.md21
-rw-r--r--doc/user/clusters/applications.md6
-rw-r--r--doc/user/infrastructure/index.md21
-rw-r--r--doc/user/project/clusters/securing.md2
-rw-r--r--doc/user/project/integrations/overview.md20
-rw-r--r--doc/user/project/integrations/services_templates.md27
-rw-r--r--doc/user/project/issues/design_management.md35
-rw-r--r--lib/api/admin/instance_clusters.rb134
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/event_filter.rb16
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml17
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml9
-rw-r--r--lib/gitlab/updated_notes_paginator.rb74
-rw-r--r--lib/product_analytics/collector_app.rb40
-rw-r--r--lib/product_analytics/event_params.rb51
-rw-r--r--locale/gitlab.pot48
-rw-r--r--spec/controllers/dashboard_controller_spec.rb14
-rw-r--r--spec/controllers/groups_controller_spec.rb12
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb48
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb60
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb23
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb23
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb84
-rw-r--r--spec/controllers/projects_controller_spec.rb12
-rw-r--r--spec/finders/notes_finder_spec.rb6
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb23
-rw-r--r--spec/fixtures/clusters/ca_certificate.pem23
-rw-r--r--spec/fixtures/clusters/chain_certificates.pem100
-rw-r--r--spec/fixtures/clusters/intermediate_certificate.pem28
-rw-r--r--spec/fixtures/clusters/root_certificate.pem49
-rw-r--r--spec/fixtures/product_analytics/event.json16
-rw-r--r--spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js76
-rw-r--r--spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb58
-rw-r--r--spec/helpers/events_helper_spec.rb32
-rw-r--r--spec/lib/event_filter_spec.rb10
-rw-r--r--spec/lib/gitlab/updated_notes_paginator_spec.rb57
-rw-r--r--spec/lib/product_analytics/event_params_spec.rb54
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb16
-rw-r--r--spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb19
-rw-r--r--spec/models/application_record_spec.rb7
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb46
-rw-r--r--spec/models/event_collection_spec.rb18
-rw-r--r--spec/models/event_spec.rb9
-rw-r--r--spec/models/product_analytics_event_spec.rb7
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb461
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb55
-rw-r--r--spec/requests/product_analytics/collector_app_attack_spec.rb41
-rw-r--r--spec/requests/product_analytics/collector_app_spec.rb73
-rw-r--r--spec/routing/import_routing_spec.rb45
-rw-r--r--spec/services/alert_management/alerts/todo/create_service_spec.rb84
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb14
-rw-r--r--spec/services/event_create_service_spec.rb16
-rw-r--r--spec/services/notification_service_spec.rb20
-rw-r--r--spec/services/resource_events/merge_into_notes_service_spec.rb2
-rw-r--r--spec/support/helpers/rack_attack_spec_helpers.rb12
-rw-r--r--spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb33
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;" }
+ &nbsp;
+ %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;" }
+ &nbsp;
+ %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} &middot; %{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