summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-11 15:08:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-11 15:08:44 +0000
commitbcc77054ee9aefd1e332e04a4189390fd5a3112e (patch)
treee6e1908c310e4733038794e932196cae0d66ba9a
parent05b5c609cb8c260b10c2eb1b92b711dc82d32c3f (diff)
downloadgitlab-ce-bcc77054ee9aefd1e332e04a4189390fd5a3112e.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js6
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js2
-rw-r--r--app/models/container_expiration_policy.rb2
-rw-r--r--app/models/incident_management/project_incident_management_setting.rb34
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/incident_management/create_issue_service.rb126
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/workers/all_queues.yml6
-rw-r--r--app/workers/concerns/worker_context.rb2
-rw-r--r--app/workers/container_expiration_policy_worker.rb10
-rw-r--r--app/workers/incident_management/process_alert_worker.rb29
-rw-r--r--changelogs/unreleased/show_selected_template_type.yml5
-rw-r--r--config/prometheus/self_monitoring_default.yml39
-rw-r--r--doc/ci/environments.md13
-rw-r--r--doc/user/project/deploy_boards.md6
-rw-r--r--spec/factories/incident_management/project_incident_management_settings.rb10
-rw-r--r--spec/features/issues/issue_detail_spec.rb8
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb5
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb150
-rw-r--r--spec/models/container_expiration_policy_spec.rb4
-rw-r--r--spec/models/incident_management/project_incident_management_setting_spec.rb111
-rw-r--r--spec/services/incident_management/create_issue_service_spec.rb311
-rw-r--r--spec/workers/concerns/worker_context_spec.rb10
-rw-r--r--spec/workers/incident_management/process_alert_worker_spec.rb39
24 files changed, 849 insertions, 83 deletions
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 2df7a84ead0..0fb02ca5965 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -117,11 +117,7 @@ export default class FileTemplateMediator {
selector.hide();
}
});
-
- if (this.editor.getValue() !== '') {
- this.setTypeSelectorToggleText(item.name);
- }
-
+ this.setTypeSelectorToggleText(item.name);
this.cacheToggleText();
}
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
index 10483abfb23..cb6d30c775d 100644
--- a/app/assets/javascripts/code_navigation/store/actions.js
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -16,7 +16,7 @@ export default {
commit(types.REQUEST_DATA);
api
- .lsifData(state.projectPath, state.commitId, state.path)
+ .lsifData(state.projectPath, state.commitId, state.blobPath)
.then(({ data }) => {
const normalizedData = data.reduce((acc, d) => {
if (d.hover) {
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index c929a78a7f9..ccb0a0f8acd 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -14,7 +14,7 @@ class ContainerExpirationPolicy < ApplicationRecord
validates :keep_n, inclusion: { in: ->(_) { self.keep_n_options.keys } }, allow_nil: true
scope :active, -> { where(enabled: true) }
- scope :preloaded, -> { preload(:project) }
+ scope :preloaded, -> { preload(project: [:route]) }
def self.keep_n_options
{
diff --git a/app/models/incident_management/project_incident_management_setting.rb b/app/models/incident_management/project_incident_management_setting.rb
new file mode 100644
index 00000000000..bf57c5b883f
--- /dev/null
+++ b/app/models/incident_management/project_incident_management_setting.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class ProjectIncidentManagementSetting < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+
+ belongs_to :project
+
+ validate :issue_template_exists, if: :create_issue?
+
+ def available_issue_templates
+ Gitlab::Template::IssueTemplate.all(project)
+ end
+
+ def issue_template_content
+ strong_memoize(:issue_template_content) do
+ issue_template&.content if issue_template_key.present?
+ end
+ end
+
+ private
+
+ def issue_template_exists
+ return unless issue_template_key.present?
+
+ errors.add(:issue_template_key, 'not found') unless issue_template
+ end
+
+ def issue_template
+ Gitlab::Template::IssueTemplate.find(issue_template_key, project)
+ rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 8454ece814f..b5639039bb6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -187,6 +187,7 @@ class Project < ApplicationRecord
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :project_repository, inverse_of: :project
+ has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
has_one :grafana_integration, inverse_of: :project
@@ -316,6 +317,7 @@ class Project < ApplicationRecord
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+ accepts_nested_attributes_for :incident_management_setting, update_only: true
accepts_nested_attributes_for :error_tracking_setting, update_only: true
accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true
diff --git a/app/services/incident_management/create_issue_service.rb b/app/services/incident_management/create_issue_service.rb
new file mode 100644
index 00000000000..94b6f037924
--- /dev/null
+++ b/app/services/incident_management/create_issue_service.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class CreateIssueService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ INCIDENT_LABEL = {
+ title: 'incident',
+ color: '#CC0033',
+ description: <<~DESCRIPTION.chomp
+ Denotes a disruption to IT services and \
+ the associated issues require immediate attention
+ DESCRIPTION
+ }.freeze
+
+ def initialize(project, params)
+ super(project, User.alert_bot, params)
+ end
+
+ def execute
+ return error_with('setting disabled') unless incident_management_setting.create_issue?
+ return error_with('invalid alert') unless alert.valid?
+
+ issue = create_issue
+ return error_with(issue_errors(issue)) unless issue.valid?
+
+ success(issue: issue)
+ end
+
+ private
+
+ def create_issue
+ issue = do_create_issue(label_ids: issue_label_ids)
+
+ # Create an unlabelled issue if we couldn't create the issue
+ # due to labels errors.
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/65042
+ if issue.errors.include?(:labels)
+ log_label_error(issue)
+ issue = do_create_issue
+ end
+
+ issue
+ end
+
+ def do_create_issue(**params)
+ Issues::CreateService.new(
+ project,
+ current_user,
+ title: issue_title,
+ description: issue_description,
+ **params
+ ).execute
+ end
+
+ def issue_title
+ alert.full_title
+ end
+
+ def issue_description
+ horizontal_line = "\n---\n\n"
+
+ [
+ alert_summary,
+ alert_markdown,
+ issue_template_content
+ ].compact.join(horizontal_line)
+ end
+
+ def issue_label_ids
+ [
+ find_or_create_label(**INCIDENT_LABEL)
+ ].compact.map(&:id)
+ end
+
+ def find_or_create_label(**params)
+ Labels::FindOrCreateService
+ .new(current_user, project, **params)
+ .execute
+ end
+
+ def alert_summary
+ alert.issue_summary_markdown
+ end
+
+ def alert_markdown
+ alert.alert_markdown
+ end
+
+ def alert
+ strong_memoize(:alert) do
+ Gitlab::Alerting::Alert.new(project: project, payload: params).present
+ end
+ end
+
+ def issue_template_content
+ incident_management_setting.issue_template_content
+ end
+
+ def incident_management_setting
+ strong_memoize(:incident_management_setting) do
+ project.incident_management_setting ||
+ project.build_incident_management_setting
+ end
+ end
+
+ def issue_errors(issue)
+ issue.errors.full_messages.to_sentence
+ end
+
+ def log_label_error(issue)
+ log_info <<~TEXT.chomp
+ Cannot create incident issue with labels \
+ #{issue.labels.map(&:title).inspect} \
+ for "#{project.full_name}": #{issue.errors.full_messages.to_sentence}.
+ Retrying without labels.
+ TEXT
+ end
+
+ def error_with(message)
+ log_error(%{Cannot create incident issue for "#{project.full_name}": #{message}})
+
+ error(message)
+ end
+ end
+end
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 9803d65c4fb..91d1fc06a41 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -10,7 +10,7 @@
#blob-content-holder.blob-content-holder
- if native_code_navigation_enabled?(@project)
- #js-code-navigation{ data: { commit_id: blob.commit_id, path: blob.path, project_path: @project.full_path } }
+ #js-code-navigation{ data: { commit_id: blob.commit_id, blob_path: blob.path, project_path: @project.full_path } }
%article.file-holder
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e836dd92770..22dd7f5843f 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -429,6 +429,12 @@
:latency_sensitive:
:resource_boundary: :unknown
:weight: 1
+- :name: incident_management:incident_management_process_alert
+ :feature_category: :incident_management
+ :has_external_dependencies:
+ :latency_sensitive:
+ :resource_boundary: :unknown
+ :weight: 2
- :name: mail_scheduler:mail_scheduler_issue_due
:feature_category: :issue_tracking
:has_external_dependencies:
diff --git a/app/workers/concerns/worker_context.rb b/app/workers/concerns/worker_context.rb
index ca006eaad5d..f2ff3ecfb6b 100644
--- a/app/workers/concerns/worker_context.rb
+++ b/app/workers/concerns/worker_context.rb
@@ -60,6 +60,6 @@ module WorkerContext
end
def with_context(context, &block)
- Gitlab::ApplicationContext.new(context).use(&block)
+ Gitlab::ApplicationContext.new(context).use { yield(**context) }
end
end
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index a930dfa0186..e07a6546e2d 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -2,15 +2,17 @@
class ContainerExpirationPolicyWorker
include ApplicationWorker
- include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+ include CronjobQueue
feature_category :container_registry
def perform
ContainerExpirationPolicy.runnable_schedules.preloaded.find_each do |container_expiration_policy|
- ContainerExpirationPolicyService.new(
- container_expiration_policy.project, container_expiration_policy.project.owner
- ).execute(container_expiration_policy)
+ with_context(project: container_expiration_policy.project,
+ user: container_expiration_policy.project.owner) do |project:, user:|
+ ContainerExpirationPolicyService.new(project, user)
+ .execute(container_expiration_policy)
+ end
end
end
end
diff --git a/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
new file mode 100644
index 00000000000..f3d5bc5c66b
--- /dev/null
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class ProcessAlertWorker
+ include ApplicationWorker
+
+ queue_namespace :incident_management
+ feature_category :incident_management
+
+ def perform(project_id, alert)
+ project = find_project(project_id)
+ return unless project
+
+ create_issue(project, alert)
+ end
+
+ private
+
+ def find_project(project_id)
+ Project.find_by_id(project_id)
+ end
+
+ def create_issue(project, alert)
+ IncidentManagement::CreateIssueService
+ .new(project, alert)
+ .execute
+ end
+ end
+end
diff --git a/changelogs/unreleased/show_selected_template_type.yml b/changelogs/unreleased/show_selected_template_type.yml
new file mode 100644
index 00000000000..018438fb2ca
--- /dev/null
+++ b/changelogs/unreleased/show_selected_template_type.yml
@@ -0,0 +1,5 @@
+---
+title: Show selected template type when clicked
+merge_request: 24596
+author:
+type: fixed
diff --git a/config/prometheus/self_monitoring_default.yml b/config/prometheus/self_monitoring_default.yml
new file mode 100644
index 00000000000..dc2361fb3bc
--- /dev/null
+++ b/config/prometheus/self_monitoring_default.yml
@@ -0,0 +1,39 @@
+dashboard: 'Default dashboard'
+priority: 1
+panel_groups:
+- group: Web Service
+ panels:
+ - title: Web Service - Error Ratio
+ type: line-chart
+ y_label: "Unhandled Exceptions (%)"
+ metrics:
+ - id: wser_web_service
+ query_range: 'max(max_over_time(gitlab_service_errors:ratio{environment="{{ci_environment_slug}}", type="web", stage="main"}[1m])) by (type) * 100'
+ unit: "%"
+ label: "Error Ratio"
+ - id: wser_degradation_slo
+ query_range: 'avg(slo:max:gitlab_service_errors:ratio{environment="{{ci_environment_slug}}", type="web", stage="main"}) or avg(slo:max:gitlab_service_errors:ratio{type="web"}) * 100'
+ unit: "%"
+ label: "Degradation SLO"
+ - id: wser_outage_slo
+ query_range: '2 * (avg(slo:max:gitlab_service_errors:ratio{environment="{{ci_environment_slug}}", type="web", stage="main"}) or avg(slo:max:gitlab_service_errors:ratio{type="web"})) * 100'
+ unit: "%"
+ label: "Outage SLO"
+- group: API Service
+ panels:
+ - title: API Service - Error Ratio
+ type: line-chart
+ y_label: "Unhandled Exceptions (%)"
+ metrics:
+ - id: aser_web_service
+ query_range: 'max(max_over_time(gitlab_service_errors:ratio{environment="{{ci_environment_slug}}", type="api", stage="main"}[1m])) by (type) * 100'
+ unit: "%"
+ label: "Error Ratio"
+ - id: aser_degradation_slo
+ query_range: 'avg(slo:max:gitlab_service_errors:ratio{environment="{{ci_environment_slug}}", type="api", stage="main"}) or avg(slo:max:gitlab_service_errors:ratio{type="web"}) * 100'
+ unit: "%"
+ label: "Degradation SLO"
+ - id: aser_outage_slo
+ query_range: '2 * (avg(slo:max:gitlab_service_errors:ratio{environment="{{ci_environment_slug}}", type="api", stage="main"}) or avg(slo:max:gitlab_service_errors:ratio{type="web"})) * 100'
+ unit: "%"
+ label: "Outage SLO"
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 61f09bf6e00..eddda2031b0 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -115,11 +115,14 @@ With this configuration, we:
- Lastly we deploy to the staging server.
NOTE: **Note:**
-The `environment` keyword is just a hint for GitLab that this job actually
-deploys to the `name` environment. It can also have a `url` that is
-exposed in various places within GitLab. Each time a job that
-has an environment specified succeeds, a deployment is recorded, storing
-the Git SHA and environment name.
+The `environment` keyword defines where the app is deployed.
+The environment `name` and `url` is exposed in various places
+within GitLab. Each time a job that has an environment specified
+succeeds, a deployment is recorded, along with the Git SHA and environment name.
+
+CAUTION: **Caution**:
+Some characters are not allowed in environment names. Use only letters,
+numbers, spaces, and `-`, `_`, `/`, `{`, `}`, or `.`. Also, it must not start nor end with `/`.
In summary, with the above `.gitlab-ci.yml` we have achieved the following:
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
index c4d357e24f0..fe2c8bbf4ed 100644
--- a/doc/user/project/deploy_boards.md
+++ b/doc/user/project/deploy_boards.md
@@ -27,7 +27,7 @@ deploy rolling out. The percentage is the percent of the pods that are updated
to the latest release.
Since Deploy Boards are tightly coupled with Kubernetes, there is some required
-knowledge. In particular you should be familiar with:
+knowledge. In particular, you should be familiar with:
- [Kubernetes pods](https://kubernetes.io/docs/concepts/workloads/pods/pod/)
- [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
@@ -37,7 +37,7 @@ knowledge. In particular you should be familiar with:
## Use cases
Since the Deploy Board is a visual representation of the Kubernetes pods for a
-specific environment, there are lot of uses cases. To name a few:
+specific environment, there are a lot of use cases. To name a few:
- You want to promote what's running in staging, to production. You go to the
environments list, verify that what's running in staging is what you think is
@@ -65,7 +65,7 @@ To display the Deploy Boards for a specific [environment] you should:
NOTE: **Running on OpenShift:**
If you are using OpenShift, ensure that you're using the `Deployment` resource
- instead of `DeploymentConfiguration`, otherwise the Deploy Boards won't render
+ instead of `DeploymentConfiguration`. Otherwise, the Deploy Boards won't render
correctly. For more information, read the
[OpenShift docs](https://docs.openshift.com/container-platform/3.7/dev_guide/deployments/kubernetes_deployments.html#kubernetes-deployments-vs-deployment-configurations)
and [GitLab issue #4584](https://gitlab.com/gitlab-org/gitlab/issues/4584).
diff --git a/spec/factories/incident_management/project_incident_management_settings.rb b/spec/factories/incident_management/project_incident_management_settings.rb
new file mode 100644
index 00000000000..5b6a71d87d5
--- /dev/null
+++ b/spec/factories/incident_management/project_incident_management_settings.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_incident_management_setting, class: 'IncidentManagement::ProjectIncidentManagementSetting' do
+ project
+ create_issue { false }
+ issue_template_key { nil }
+ send_email { false }
+ end
+end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index a1b53718577..0d24b02a64c 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -23,16 +23,18 @@ describe 'Issue Detail', :js do
context 'when issue description has xss snippet' do
before do
issue.update!(description: '![xss" onload=alert(1);//](a)')
+
sign_in(user)
visit project_issue_path(project, issue)
- wait_for_requests
end
it 'encodes the description to prevent xss issues' do
page.within('.issuable-details .detail-page-description') do
+ image = find('img.js-lazy-loaded')
+
expect(page).to have_selector('img', count: 1)
- expect(find('img')['onerror']).to be_nil
- expect(find('img')['src']).to end_with('/a')
+ expect(image['onerror']).to be_nil
+ expect(image['src']).to end_with('/a')
end
end
end
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index ba52a7e7deb..03b4b9b4517 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -75,6 +75,11 @@ describe 'Projects > Files > Template type dropdown selector', :js do
check_type_selector_toggle_text('.gitignore')
end
+ it 'sets the toggle text when selecting the template type' do
+ select_template_type('.gitignore')
+ check_type_selector_toggle_text('.gitignore')
+ end
+
it 'selects every template type correctly' do
try_selecting_all_types
end
diff --git a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb
index f617e8b3ce7..c193ab2b50f 100644
--- a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb
@@ -103,7 +103,75 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
end
end
- context 'when multiple orders are defined' do
+ shared_examples 'nodes are in ascending order' do
+ context 'when no cursor is passed' do
+ let(:arguments) { {} }
+
+ it 'returns projects in ascending order' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes)
+ end
+ end
+
+ context 'when before cursor value is not NULL' do
+ let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes.first(2))
+ end
+ end
+
+ context 'when after cursor value is not NULL' do
+ let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes.last(3))
+ end
+ end
+
+ context 'when before and after cursor' do
+ let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(ascending_nodes[1..3])
+ end
+ end
+ end
+
+ shared_examples 'nodes are in descending order' do
+ context 'when no cursor is passed' do
+ let(:arguments) { {} }
+
+ it 'only returns projects in descending order' do
+ expect(subject.sliced_nodes).to eq(descending_nodes)
+ end
+ end
+
+ context 'when before cursor value is not NULL' do
+ let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq(descending_nodes.first(2))
+ end
+ end
+
+ context 'when after cursor value is not NULL' do
+ let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(descending_nodes.last(3))
+ end
+ end
+
+ context 'when before and after cursor' do
+ let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq(descending_nodes[1..3])
+ end
+ end
+ end
+
+ context 'when multiple orders with nil values are defined' do
let!(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3
let!(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1
let!(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5
@@ -114,14 +182,9 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
let(:nodes) do
Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :asc).order(id: :asc)
end
+ let(:ascending_nodes) { [project5, project1, project3, project2, project4] }
- context 'when no cursor is passed' do
- let(:arguments) { {} }
-
- it 'returns projects in ascending order' do
- expect(subject.sliced_nodes).to eq([project5, project1, project3, project2, project4])
- end
- end
+ it_behaves_like 'nodes are in ascending order'
context 'when before cursor value is NULL' do
let(:arguments) { { before: encoded_cursor(project4) } }
@@ -131,14 +194,6 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
end
end
- context 'when before cursor value is not NULL' do
- let(:arguments) { { before: encoded_cursor(project3) } }
-
- it 'returns all projects before the cursor' do
- expect(subject.sliced_nodes).to eq([project5, project1])
- end
- end
-
context 'when after cursor value is NULL' do
let(:arguments) { { after: encoded_cursor(project2) } }
@@ -146,36 +201,15 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
expect(subject.sliced_nodes).to eq([project4])
end
end
-
- context 'when after cursor value is not NULL' do
- let(:arguments) { { after: encoded_cursor(project1) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq([project3, project2, project4])
- end
- end
-
- context 'when before and after cursor' do
- let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project5) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq([project1, project3, project2])
- end
- end
end
context 'when descending' do
let(:nodes) do
Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :desc).order(id: :asc)
end
+ let(:descending_nodes) { [project3, project1, project5, project2, project4] }
- context 'when no cursor is passed' do
- let(:arguments) { {} }
-
- it 'only returns projects in descending order' do
- expect(subject.sliced_nodes).to eq([project3, project1, project5, project2, project4])
- end
- end
+ it_behaves_like 'nodes are in descending order'
context 'when before cursor value is NULL' do
let(:arguments) { { before: encoded_cursor(project4) } }
@@ -185,14 +219,6 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
end
end
- context 'when before cursor value is not NULL' do
- let(:arguments) { { before: encoded_cursor(project5) } }
-
- it 'returns all projects before the cursor' do
- expect(subject.sliced_nodes).to eq([project3, project1])
- end
- end
-
context 'when after cursor value is NULL' do
let(:arguments) { { after: encoded_cursor(project2) } }
@@ -200,22 +226,32 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
expect(subject.sliced_nodes).to eq([project4])
end
end
+ end
+ end
- context 'when after cursor value is not NULL' do
- let(:arguments) { { after: encoded_cursor(project1) } }
+ context 'when ordering uses LOWER' do
+ let!(:project1) { create(:project, name: 'A') } # Asc: project1 Desc: project4
+ let!(:project2) { create(:project, name: 'c') } # Asc: project5 Desc: project2
+ let!(:project3) { create(:project, name: 'b') } # Asc: project3 Desc: project3
+ let!(:project4) { create(:project, name: 'd') } # Asc: project2 Desc: project5
+ let!(:project5) { create(:project, name: 'a') } # Asc: project4 Desc: project1
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq([project5, project2, project4])
- end
+ context 'when ascending' do
+ let(:nodes) do
+ Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(id: :asc)
end
+ let(:ascending_nodes) { [project1, project5, project3, project2, project4] }
- context 'when before and after cursor' do
- let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project3) } }
+ it_behaves_like 'nodes are in ascending order'
+ end
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq([project1, project5, project2])
- end
+ context 'when descending' do
+ let(:nodes) do
+ Project.order(Arel::Table.new(:projects)['name'].lower.desc).order(id: :desc)
end
+ let(:descending_nodes) { [project4, project2, project3, project5, project1] }
+
+ it_behaves_like 'nodes are in descending order'
end
end
diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb
index 1bce4c3b20a..c22362ed5d4 100644
--- a/spec/models/container_expiration_policy_spec.rb
+++ b/spec/models/container_expiration_policy_spec.rb
@@ -49,9 +49,9 @@ RSpec.describe ContainerExpirationPolicy, type: :model do
it 'preloads the associations' do
subject
- query = ActiveRecord::QueryRecorder.new { subject.each(&:project) }
+ query = ActiveRecord::QueryRecorder.new { subject.map(&:project).map(&:full_path) }
- expect(query.count).to eq(2)
+ expect(query.count).to eq(3)
end
end
diff --git a/spec/models/incident_management/project_incident_management_setting_spec.rb b/spec/models/incident_management/project_incident_management_setting_spec.rb
new file mode 100644
index 00000000000..ac3f97e2d89
--- /dev/null
+++ b/spec/models/incident_management/project_incident_management_setting_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IncidentManagement::ProjectIncidentManagementSetting do
+ let_it_be(:project) { create(:project, :repository, create_templates: :issue) }
+
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ describe 'validate issue_template_exists' do
+ subject { build(:project_incident_management_setting, project: project) }
+
+ context 'with create_issue enabled' do
+ before do
+ subject.create_issue = true
+ end
+
+ context 'with valid issue_template_key' do
+ before do
+ subject.issue_template_key = 'bug'
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with empty issue_template_key' do
+ before do
+ subject.issue_template_key = ''
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with nil issue_template_key' do
+ before do
+ subject.issue_template_key = nil
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with invalid issue_template_key' do
+ before do
+ subject.issue_template_key = 'unknown'
+ end
+
+ it { is_expected.to be_invalid }
+
+ it 'returns error' do
+ subject.valid?
+
+ expect(subject.errors[:issue_template_key]).to eq(['not found'])
+ end
+ end
+ end
+
+ context 'with create_issue disabled' do
+ before do
+ subject.create_issue = false
+ end
+
+ context 'with unknown issue_template_key' do
+ before do
+ subject.issue_template_key = 'unknown'
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+ end
+
+ describe '#issue_template_content' do
+ subject { build(:project_incident_management_setting, project: project) }
+
+ shared_examples 'no content' do
+ it 'returns no content' do
+ expect(subject.issue_template_content).to be_nil
+ end
+ end
+
+ context 'with valid issue_template_key' do
+ before do
+ subject.issue_template_key = 'bug'
+ end
+
+ it 'returns issue content' do
+ expect(subject.issue_template_content).to eq('something valid')
+ end
+ end
+
+ context 'with unknown issue_template_key' do
+ before do
+ subject.issue_template_key = 'unknown'
+ end
+
+ it_behaves_like 'no content'
+ end
+
+ context 'without issue_template_key' do
+ before do
+ subject.issue_template_key = nil
+ end
+
+ it_behaves_like 'no content'
+ end
+ end
+end
diff --git a/spec/services/incident_management/create_issue_service_spec.rb b/spec/services/incident_management/create_issue_service_spec.rb
new file mode 100644
index 00000000000..e720aafb897
--- /dev/null
+++ b/spec/services/incident_management/create_issue_service_spec.rb
@@ -0,0 +1,311 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IncidentManagement::CreateIssueService do
+ let(:project) { create(:project, :repository, :private) }
+ let(:user) { User.alert_bot }
+ let(:service) { described_class.new(project, alert_payload) }
+ let(:alert_starts_at) { Time.now }
+ let(:alert_title) { 'TITLE' }
+ let(:alert_annotations) { { title: alert_title } }
+
+ let(:alert_payload) do
+ build_alert_payload(
+ annotations: alert_annotations,
+ starts_at: alert_starts_at
+ )
+ end
+
+ let(:alert_presenter) do
+ Gitlab::Alerting::Alert.new(project: project, payload: alert_payload).present
+ end
+
+ let!(:setting) do
+ create(:project_incident_management_setting, project: project)
+ end
+
+ subject { service.execute }
+
+ context 'when create_issue enabled' do
+ let(:issue) { subject[:issue] }
+ let(:summary_separator) { "\n---\n\n" }
+
+ before do
+ setting.update!(create_issue: true)
+ end
+
+ context 'without issue_template_content' do
+ it 'creates an issue with alert summary only' do
+ expect(subject).to include(status: :success)
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq(alert_title)
+ expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip)
+ expect(separator_count(issue.description)).to eq 0
+ end
+ end
+
+ context 'with erroneous issue service' do
+ let(:invalid_issue) do
+ build(:issue, project: project, title: nil).tap(&:valid?)
+ end
+
+ let(:issue_error) { invalid_issue.errors.full_messages.to_sentence }
+
+ it 'returns and logs the issue error' do
+ expect_next_instance_of(Issues::CreateService) do |issue_service|
+ expect(issue_service).to receive(:execute).and_return(invalid_issue)
+ end
+
+ expect(service)
+ .to receive(:log_error)
+ .with(error_message(issue_error))
+
+ expect(subject).to include(status: :error, message: issue_error)
+ end
+ end
+
+ shared_examples 'GFM template' do
+ context 'plain content' do
+ let(:template_content) { 'some content' }
+
+ it 'creates an issue appending issue template' do
+ expect(subject).to include(status: :success)
+
+ expect(issue.description).to include(alert_presenter.issue_summary_markdown)
+ expect(separator_count(issue.description)).to eq 1
+ expect(issue.description).to include(template_content)
+ end
+ end
+
+ context 'quick actions' do
+ let(:user) { create(:user) }
+ let(:plain_text) { 'some content' }
+
+ let(:template_content) do
+ <<~CONTENT
+ #{plain_text}
+ /due tomorrow
+ /assign @#{user.username}
+ CONTENT
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates an issue interpreting quick actions' do
+ expect(subject).to include(status: :success)
+
+ expect(issue.description).to include(plain_text)
+ expect(issue.due_date).to be_present
+ expect(issue.assignees).to eq([user])
+ end
+ end
+ end
+
+ context 'with gitlab_incident_markdown' do
+ let(:alert_annotations) do
+ { title: alert_title, gitlab_incident_markdown: template_content }
+ end
+
+ it_behaves_like 'GFM template'
+ end
+
+ context 'with issue_template_content' do
+ before do
+ create_issue_template('bug', template_content)
+ setting.update!(issue_template_key: 'bug')
+ end
+
+ it_behaves_like 'GFM template'
+
+ context 'and gitlab_incident_markdown' do
+ let(:template_content) { 'plain text'}
+ let(:alt_template) { 'alternate text' }
+ let(:alert_annotations) do
+ { title: alert_title, gitlab_incident_markdown: alt_template }
+ end
+
+ it 'includes both templates' do
+ expect(subject).to include(status: :success)
+
+ expect(issue.description).to include(alert_presenter.issue_summary_markdown)
+ expect(issue.description).to include(template_content)
+ expect(issue.description).to include(alt_template)
+ expect(separator_count(issue.description)).to eq 2
+ end
+ end
+
+ private
+
+ def create_issue_template(name, content)
+ project.repository.create_file(
+ project.creator,
+ ".gitlab/issue_templates/#{name}.md",
+ content,
+ message: 'message',
+ branch_name: 'master'
+ )
+ end
+ end
+
+ context 'with gitlab alert' do
+ let(:gitlab_alert) { create(:prometheus_alert, project: project) }
+
+ before do
+ alert_payload['labels'] = {
+ 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s
+ }
+ end
+
+ it 'creates an issue' do
+ query_title = "#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold}"
+
+ expect(subject).to include(status: :success)
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq(alert_presenter.full_title)
+ expect(issue.title).to include(gitlab_alert.environment.name)
+ expect(issue.title).to include(query_title)
+ expect(issue.title).to include('for 5 minutes')
+ expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip)
+ expect(separator_count(issue.description)).to eq 0
+ end
+ end
+
+ describe 'with invalid alert payload' do
+ shared_examples 'invalid alert' do
+ it 'does not create an issue' do
+ expect(service)
+ .to receive(:log_error)
+ .with(error_message('invalid alert'))
+
+ expect(subject).to eq(status: :error, message: 'invalid alert')
+ end
+ end
+
+ context 'without title' do
+ let(:alert_annotations) { {} }
+
+ it_behaves_like 'invalid alert'
+ end
+
+ context 'without startsAt' do
+ let(:alert_starts_at) { nil }
+
+ it_behaves_like 'invalid alert'
+ end
+ end
+
+ describe "label `incident`" do
+ let(:title) { 'incident' }
+ let(:color) { '#CC0033' }
+ let(:description) do
+ <<~DESCRIPTION.chomp
+ Denotes a disruption to IT services and \
+ the associated issues require immediate attention
+ DESCRIPTION
+ end
+
+ shared_examples 'existing label' do
+ it 'adds the existing label' do
+ expect { subject }.not_to change(Label, :count)
+
+ expect(issue.labels).to eq([label])
+ end
+ end
+
+ shared_examples 'new label' do
+ it 'adds newly created label' do
+ expect { subject }.to change(Label, :count).by(1)
+
+ label = project.reload.labels.last
+ expect(issue.labels).to eq([label])
+ expect(label.title).to eq(title)
+ expect(label.color).to eq(color)
+ expect(label.description).to eq(description)
+ end
+ end
+
+ context 'with predefined project label' do
+ it_behaves_like 'existing label' do
+ let!(:label) { create(:label, project: project, title: title) }
+ end
+ end
+
+ context 'with predefined group label' do
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group) }
+
+ it_behaves_like 'existing label' do
+ let!(:label) { create(:group_label, group: group, title: title) }
+ end
+ end
+
+ context 'without label' do
+ it_behaves_like 'new label'
+ end
+
+ context 'with duplicate labels', issue: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/65042' do
+ before do
+ # Replicate race condition to create duplicates
+ build(:label, project: project, title: title).save!(validate: false)
+ build(:label, project: project, title: title).save!(validate: false)
+ end
+
+ it 'create an issue without labels' do
+ # Verify we have duplicates
+ expect(project.labels.size).to eq(2)
+ expect(project.labels.map(&:title)).to all(eq(title))
+
+ message = <<~MESSAGE.chomp
+ Cannot create incident issue with labels ["#{title}"] for \
+ "#{project.full_name}": Labels is invalid.
+ Retrying without labels.
+ MESSAGE
+
+ expect(service)
+ .to receive(:log_info)
+ .with(message)
+
+ expect(subject).to include(status: :success)
+ expect(issue.labels).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when create_issue disabled' do
+ before do
+ setting.update!(create_issue: false)
+ end
+
+ it 'returns an error' do
+ expect(service)
+ .to receive(:log_error)
+ .with(error_message('setting disabled'))
+
+ expect(subject).to eq(status: :error, message: 'setting disabled')
+ end
+ end
+
+ private
+
+ def build_alert_payload(annotations: {}, starts_at: Time.now)
+ {
+ 'annotations' => annotations.stringify_keys
+ }.tap do |payload|
+ payload['startsAt'] = starts_at.rfc3339 if starts_at
+ end
+ end
+
+ def error_message(message)
+ %{Cannot create incident issue for "#{project.full_name}": #{message}}
+ end
+
+ def separator_count(text)
+ text.scan(summary_separator).size
+ end
+end
diff --git a/spec/workers/concerns/worker_context_spec.rb b/spec/workers/concerns/worker_context_spec.rb
index 97a88eecd73..4e8c81c57dc 100644
--- a/spec/workers/concerns/worker_context_spec.rb
+++ b/spec/workers/concerns/worker_context_spec.rb
@@ -106,5 +106,15 @@ describe WorkerContext do
expect(Labkit::Context.current.to_h).to include('meta.user' => 'jane-doe')
end
end
+
+ it 'yields the arguments to the block' do
+ a_user = build_stubbed(:user)
+ a_project = build_stubbed(:project)
+
+ worker.new.with_context(user: a_user, project: a_project) do |user:, project:|
+ expect(user).to eq(a_user)
+ expect(project).to eq(a_project)
+ end
+ end
end
end
diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb
new file mode 100644
index 00000000000..9f40833dfd7
--- /dev/null
+++ b/spec/workers/incident_management/process_alert_worker_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IncidentManagement::ProcessAlertWorker do
+ let_it_be(:project) { create(:project) }
+
+ describe '#perform' do
+ let(:alert) { :alert }
+ let(:create_issue_service) { spy(:create_issue_service) }
+
+ subject { described_class.new.perform(project.id, alert) }
+
+ it 'calls create issue service' do
+ expect(Project).to receive(:find_by_id).and_call_original
+
+ expect(IncidentManagement::CreateIssueService)
+ .to receive(:new).with(project, :alert)
+ .and_return(create_issue_service)
+
+ expect(create_issue_service).to receive(:execute)
+
+ subject
+ end
+
+ context 'with invalid project' do
+ let(:invalid_project_id) { 0 }
+
+ subject { described_class.new.perform(invalid_project_id, alert) }
+
+ it 'does not create issues' do
+ expect(Project).to receive(:find_by_id).and_call_original
+ expect(IncidentManagement::CreateIssueService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end