Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
05b5c609cb
commit
bcc77054ee
24 changed files with 849 additions and 83 deletions
|
@ -117,11 +117,7 @@ export default class FileTemplateMediator {
|
|||
selector.hide();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.editor.getValue() !== '') {
|
||||
this.setTypeSelectorToggleText(item.name);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
126
app/services/incident_management/create_issue_service.rb
Normal file
126
app/services/incident_management/create_issue_service.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
29
app/workers/incident_management/process_alert_worker.rb
Normal file
29
app/workers/incident_management/process_alert_worker.rb
Normal file
|
@ -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
|
5
changelogs/unreleased/show_selected_template_type.yml
Normal file
5
changelogs/unreleased/show_selected_template_type.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show selected template type when clicked
|
||||
merge_request: 24596
|
||||
author:
|
||||
type: fixed
|
39
config/prometheus/self_monitoring_default.yml
Normal file
39
config/prometheus/self_monitoring_default.yml
Normal file
|
@ -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"
|
|
@ -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).
|
||||
|
|
|
@ -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
|
|
@ -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,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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
311
spec/services/incident_management/create_issue_service_spec.rb
Normal file
311
spec/services/incident_management/create_issue_service_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue