Commit bcc77054 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 05b5c609
......@@ -117,11 +117,7 @@ export default class FileTemplateMediator {
selector.hide();
}
});
if (this.editor.getValue() !== '') {
this.setTypeSelectorToggleText(item.name);
}
this.cacheToggleText();
}
......
......@@ -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) {
......
......@@ -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
{
......
# 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
......@@ -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
......
# 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
......@@ -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
......@@ -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:
......
......@@ -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
......@@ -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
# 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
---
title: Show selected template type when clicked
merge_request: 24596
author:
type: fixed
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"
......@@ -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:
......
......@@ -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).
......
# 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
......@@ -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
......
......@@ -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
......
......@@ -103,93 +103,119 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
end
end
context 'when multiple orders 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
let!(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2
let!(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4
context 'when ascending' 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
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([project5, project1, project3, project2, project4])
expect(subject.sliced_nodes).to eq(ascending_nodes)
end
end
context 'when before cursor value is NULL' do
let(:arguments) { { before: encoded_cursor(project4) } }
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([project5, project1, project3, project2])
expect(subject.sliced_nodes).to eq(ascending_nodes.first(2))
end
end
context 'when before cursor value is not NULL' do
let(:arguments) { { before: encoded_cursor(project3) } }
context 'when after cursor value is not NULL' do
let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } }
it 'returns all projects before the cursor' do
expect(subject.sliced_nodes).to eq([project5, project1])
it 'returns all projects after the cursor' do
expect(subject.sliced_nodes).to eq(ascending_nodes.last(3))
end
end
context 'when after cursor value is NULL' do
let(:arguments) { { after: encoded_cursor(project2) } }
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([project4])
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(project1) } }
let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } }
it 'returns all projects after the cursor' do
expect(subject.sliced_nodes).to eq([project3, project2, project4])
expect(subject.sliced_nodes).to eq(descending_nodes.last(3))
end
end
context 'when before and after cursor' do
let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project5) } }
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([project1, project3, project2])
expect(subject.sliced_nodes).to eq(descending_nodes[1..3])
end
end
end
context 'when descending' do
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
let!(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2
let!(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4
context 'when ascending' do
let(:nodes) do
Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :desc).order(id: :asc)
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 '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 ascending order'
context 'when before cursor value is NULL' do
let(:arguments) { { before: encoded_cursor(project4) } }
it 'returns all projects before the cursor' do
expect(subject.sliced_nodes).to eq([project3, project1, project5, project2])
expect(subject.sliced_nodes).to eq([project5, project1, project3, project2])
end
end
context 'when before cursor value is not NULL' do
let(:arguments) { { before: encoded_cursor(project5) } }
context 'when after cursor value is NULL' do
let(:arguments) { { after: encoded_cursor(project2) } }
it 'returns all projects after the cursor' do
expect(subject.sliced_nodes).to eq([project4])
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] }
it_behaves_like 'nodes are in descending order'
context 'when before cursor value is NULL' do
let(:arguments) { { before: encoded_cursor(project4) } }
it 'returns all projects before the cursor' do
expect(subject.sliced_nodes).to eq([project3, project1])
expect(subject.sliced_nodes).to eq([project3, project1, project5, project2])
end
end
......@@ -200,22 +226,32 @@ 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([project5, project2, project4])
end
end
context 'when before and after cursor' do
let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project3) } }
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([project1, project5, project2])
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] }
it_behaves_like 'nodes are in ascending order'
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
......
......@@ -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
......
# 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
# 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
......@@ -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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment