Commit 6d313255 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '262847-create-on-call-schedules' into 'master'

Create On-call schedules via GraphQL

See merge request gitlab-org/gitlab!47407
parents 04fabe2a b7cf298e
...@@ -15,7 +15,8 @@ module Enums ...@@ -15,7 +15,8 @@ module Enums
operations_user_lists: 7, operations_user_lists: 7,
alert_management_alerts: 8, alert_management_alerts: 8,
sprints: 9, # iterations sprints: 9, # iterations
design_management_designs: 10 design_management_designs: 10,
incident_management_oncall_schedules: 11
} }
end end
end end
......
---
title: Create `incident_management_oncall_schedules` table
merge_request: 47407
author:
type: added
# frozen_string_literal: true
class CreateIncidentManagementOncallSchedules < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
unless table_exists?(:incident_management_oncall_schedules)
create_table :incident_management_oncall_schedules do |t|
t.timestamps_with_timezone
t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
t.integer :iid, null: false
t.text :name, null: false
t.text :description
t.text :timezone
t.index %w(project_id iid), name: 'index_im_oncall_schedules_on_project_id_and_iid', unique: true, using: :btree
end
end
end
add_text_limit :incident_management_oncall_schedules, :name, 200
add_text_limit :incident_management_oncall_schedules, :description, 1000
add_text_limit :incident_management_oncall_schedules, :timezone, 100
end
def down
with_lock_retries do
drop_table :incident_management_oncall_schedules
end
end
end
0efb2dcfc65da007a67a15857d0a283dad301650f999a4227aa54ea00dca24bf
\ No newline at end of file
...@@ -12858,6 +12858,29 @@ CREATE SEQUENCE import_failures_id_seq ...@@ -12858,6 +12858,29 @@ CREATE SEQUENCE import_failures_id_seq
ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id; ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id;
CREATE TABLE incident_management_oncall_schedules (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
project_id bigint NOT NULL,
iid integer NOT NULL,
name text NOT NULL,
description text,
timezone text,
CONSTRAINT check_7ed1fd5aa7 CHECK ((char_length(description) <= 1000)),
CONSTRAINT check_cc77cbb103 CHECK ((char_length(timezone) <= 100)),
CONSTRAINT check_e6ef43a664 CHECK ((char_length(name) <= 200))
);
CREATE SEQUENCE incident_management_oncall_schedules_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE incident_management_oncall_schedules_id_seq OWNED BY incident_management_oncall_schedules.id;
CREATE TABLE index_statuses ( CREATE TABLE index_statuses (
id integer NOT NULL, id integer NOT NULL,
project_id integer NOT NULL, project_id integer NOT NULL,
...@@ -18031,6 +18054,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo ...@@ -18031,6 +18054,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo
ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass); ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_schedules ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_schedules_id_seq'::regclass);
ALTER TABLE ONLY index_statuses ALTER COLUMN id SET DEFAULT nextval('index_statuses_id_seq'::regclass); ALTER TABLE ONLY index_statuses ALTER COLUMN id SET DEFAULT nextval('index_statuses_id_seq'::regclass);
ALTER TABLE ONLY insights ALTER COLUMN id SET DEFAULT nextval('insights_id_seq'::regclass); ALTER TABLE ONLY insights ALTER COLUMN id SET DEFAULT nextval('insights_id_seq'::regclass);
...@@ -19215,6 +19240,9 @@ ALTER TABLE ONLY import_export_uploads ...@@ -19215,6 +19240,9 @@ ALTER TABLE ONLY import_export_uploads
ALTER TABLE ONLY import_failures ALTER TABLE ONLY import_failures
ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id); ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_schedules
ADD CONSTRAINT incident_management_oncall_schedules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY index_statuses ALTER TABLE ONLY index_statuses
ADD CONSTRAINT index_statuses_pkey PRIMARY KEY (id); ADD CONSTRAINT index_statuses_pkey PRIMARY KEY (id);
...@@ -21099,6 +21127,8 @@ CREATE INDEX index_identities_on_saml_provider_id ON identities USING btree (sam ...@@ -21099,6 +21127,8 @@ CREATE INDEX index_identities_on_saml_provider_id ON identities USING btree (sam
CREATE INDEX index_identities_on_user_id ON identities USING btree (user_id); CREATE INDEX index_identities_on_user_id ON identities USING btree (user_id);
CREATE UNIQUE INDEX index_im_oncall_schedules_on_project_id_and_iid ON incident_management_oncall_schedules USING btree (project_id, iid);
CREATE UNIQUE INDEX index_import_export_uploads_on_group_id ON import_export_uploads USING btree (group_id) WHERE (group_id IS NOT NULL); CREATE UNIQUE INDEX index_import_export_uploads_on_group_id ON import_export_uploads USING btree (group_id) WHERE (group_id IS NOT NULL);
CREATE INDEX index_import_export_uploads_on_project_id ON import_export_uploads USING btree (project_id); CREATE INDEX index_import_export_uploads_on_project_id ON import_export_uploads USING btree (project_id);
...@@ -21115,6 +21145,8 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI ...@@ -21115,6 +21145,8 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI
CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL); CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL);
CREATE INDEX index_incident_management_oncall_schedules_on_project_id ON incident_management_oncall_schedules USING btree (project_id);
CREATE UNIQUE INDEX index_index_statuses_on_project_id ON index_statuses USING btree (project_id); CREATE UNIQUE INDEX index_index_statuses_on_project_id ON index_statuses USING btree (project_id);
CREATE INDEX index_insights_on_namespace_id ON insights USING btree (namespace_id); CREATE INDEX index_insights_on_namespace_id ON insights USING btree (namespace_id);
...@@ -23587,6 +23619,9 @@ ALTER TABLE ONLY open_project_tracker_data ...@@ -23587,6 +23619,9 @@ ALTER TABLE ONLY open_project_tracker_data
ALTER TABLE ONLY gpg_signatures ALTER TABLE ONLY gpg_signatures
ADD CONSTRAINT fk_rails_19d4f1c6f9 FOREIGN KEY (gpg_key_subkey_id) REFERENCES gpg_key_subkeys(id) ON DELETE SET NULL; ADD CONSTRAINT fk_rails_19d4f1c6f9 FOREIGN KEY (gpg_key_subkey_id) REFERENCES gpg_key_subkeys(id) ON DELETE SET NULL;
ALTER TABLE ONLY incident_management_oncall_schedules
ADD CONSTRAINT fk_rails_19e83fdd65 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_user_mentions ALTER TABLE ONLY vulnerability_user_mentions
ADD CONSTRAINT fk_rails_1a41c485cd FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_1a41c485cd FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
......
...@@ -10134,6 +10134,66 @@ An ISO 8601-encoded date ...@@ -10134,6 +10134,66 @@ An ISO 8601-encoded date
""" """
scalar ISO8601Date scalar ISO8601Date
"""
Describes an incident management on-call schedule
"""
type IncidentManagementOncallSchedule {
"""
Description of the on-call schedule
"""
description: String
"""
Internal ID of the on-call schedule
"""
iid: ID!
"""
Name of the on-call schedule
"""
name: String!
"""
Time zone of the on-call schedule
"""
timezone: String!
}
"""
The connection type for IncidentManagementOncallSchedule.
"""
type IncidentManagementOncallScheduleConnection {
"""
A list of edges.
"""
edges: [IncidentManagementOncallScheduleEdge]
"""
A list of nodes.
"""
nodes: [IncidentManagementOncallSchedule]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type IncidentManagementOncallScheduleEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: IncidentManagementOncallSchedule
}
type InstanceSecurityDashboard { type InstanceSecurityDashboard {
""" """
Projects selected in Instance Security Dashboard Projects selected in Instance Security Dashboard
...@@ -13890,6 +13950,7 @@ type Mutation { ...@@ -13890,6 +13950,7 @@ type Mutation {
""" """
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload
pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
...@@ -14466,6 +14527,56 @@ Identifier of Noteable ...@@ -14466,6 +14527,56 @@ Identifier of Noteable
""" """
scalar NoteableID scalar NoteableID
"""
Autogenerated input type of OncallScheduleCreate
"""
input OncallScheduleCreateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The description of the on-call schedule
"""
description: String
"""
The name of the on-call schedule
"""
name: String!
"""
The project to create the on-call schedule in
"""
projectPath: ID!
"""
The timezone of the on-call schedule
"""
timezone: String!
}
"""
Autogenerated return type of OncallScheduleCreate
"""
type OncallScheduleCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The on-call schedule
"""
oncallSchedule: IncidentManagementOncallSchedule
}
""" """
Represents a package Represents a package
""" """
...@@ -15537,6 +15648,31 @@ type Project { ...@@ -15537,6 +15648,31 @@ type Project {
""" """
importStatus: String importStatus: String
"""
Incident Management On-call schedules of the project
"""
incidentManagementOncallSchedules(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): IncidentManagementOncallScheduleConnection
""" """
A single issue of the project A single issue of the project
""" """
......
...@@ -1563,6 +1563,17 @@ Autogenerated return type of HttpIntegrationUpdate. ...@@ -1563,6 +1563,17 @@ Autogenerated return type of HttpIntegrationUpdate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `integration` | AlertManagementHttpIntegration | The HTTP integration | | `integration` | AlertManagementHttpIntegration | The HTTP integration |
### IncidentManagementOncallSchedule
Describes an incident management on-call schedule.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `description` | String | Description of the on-call schedule |
| `iid` | ID! | Internal ID of the on-call schedule |
| `name` | String! | Name of the on-call schedule |
| `timezone` | String! | Time zone of the on-call schedule |
### InstanceSecurityDashboard ### InstanceSecurityDashboard
| Field | Type | Description | | Field | Type | Description |
...@@ -2207,6 +2218,16 @@ Autogenerated return type of NamespaceIncreaseStorageTemporarily. ...@@ -2207,6 +2218,16 @@ Autogenerated return type of NamespaceIncreaseStorageTemporarily.
| `repositionNote` | Boolean! | Indicates the user can perform `reposition_note` on this resource | | `repositionNote` | Boolean! | Indicates the user can perform `reposition_note` on this resource |
| `resolveNote` | Boolean! | Indicates the user can perform `resolve_note` on this resource | | `resolveNote` | Boolean! | Indicates the user can perform `resolve_note` on this resource |
### OncallScheduleCreatePayload
Autogenerated return type of OncallScheduleCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallSchedule` | IncidentManagementOncallSchedule | The on-call schedule |
### Package ### Package
Represents a package. Represents a package.
...@@ -2351,6 +2372,7 @@ Autogenerated return type of PipelineRetry. ...@@ -2351,6 +2372,7 @@ Autogenerated return type of PipelineRetry.
| `httpUrlToRepo` | String | URL to connect to the project via HTTPS | | `httpUrlToRepo` | String | URL to connect to the project via HTTPS |
| `id` | ID! | ID of the project | | `id` | ID! | ID of the project |
| `importStatus` | String | Status of import background job of the project | | `importStatus` | String | Status of import background job of the project |
| `incidentManagementOncallSchedules` | IncidentManagementOncallScheduleConnection | Incident Management On-call schedules of the project |
| `issue` | Issue | A single issue of the project | | `issue` | Issue | A single issue of the project |
| `issueStatusCounts` | IssueStatusCountsType | Counts of issues by status for the project | | `issueStatusCounts` | IssueStatusCountsType | Counts of issues by status for the project |
| `issues` | IssueConnection | Issues of the project | | `issues` | IssueConnection | Issues of the project |
......
# frozen_string_literal: true
module IncidentManagement
class OncallSchedulesFinder
def initialize(current_user, project, params = {})
@current_user = current_user
@project = project
@params = params
end
def execute
return IncidentManagement::OncallSchedule.none unless available? && allowed?
project.incident_management_oncall_schedules
end
private
attr_reader :current_user, :project, :params
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
def allowed?
Ability.allowed?(current_user, :read_incident_management_oncall_schedule, project)
end
end
end
...@@ -48,6 +48,7 @@ module EE ...@@ -48,6 +48,7 @@ module EE
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Update
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
prepend(Types::DeprecatedMutations) prepend(Types::DeprecatedMutations)
end end
......
...@@ -148,6 +148,12 @@ module EE ...@@ -148,6 +148,12 @@ module EE
description: 'Code coverage summary associated with the project', description: 'Code coverage summary associated with the project',
resolver: ::Resolvers::Ci::CodeCoverageSummaryResolver resolver: ::Resolvers::Ci::CodeCoverageSummaryResolver
field :incident_management_oncall_schedules,
::Types::IncidentManagement::OncallScheduleType.connection_type,
null: true,
description: 'Incident Management On-call schedules of the project',
resolver: ::Resolvers::IncidentManagement::OncallScheduleResolver
def self.sast_ci_configuration(project) def self.sast_ci_configuration(project)
::Security::CiConfiguration::SastParserService.new(project).configuration ::Security::CiConfiguration::SastParserService.new(project).configuration
end end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class Create < OncallScheduleBase
include ResolvesProject
graphql_name 'OncallScheduleCreate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to create the on-call schedule in'
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'The name of the on-call schedule'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'The description of the on-call schedule'
argument :timezone, GraphQL::STRING_TYPE,
required: true,
description: 'The timezone of the on-call schedule'
def resolve(args)
project = authorized_find!(full_path: args[:project_path])
response ::IncidentManagement::OncallSchedules::CreateService.new(
project,
current_user,
args.slice(:name, :description, :timezone)
).execute
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module IncidentManagement
module OncallSchedule
class OncallScheduleBase < BaseMutation
field :oncall_schedule,
::Types::IncidentManagement::OncallScheduleType,
null: true,
description: 'The on-call schedule'
authorize :admin_incident_management_oncall_schedule
private
def response(result)
{
oncall_schedule: result.payload[:oncall_schedule],
errors: result.errors
}
end
end
end
end
end
# frozen_string_literal: true
module Resolvers
module IncidentManagement
class OncallScheduleResolver < BaseResolver
alias_method :project, :synchronized_object
type Types::IncidentManagement::OncallScheduleType.connection_type, null: true
def resolve(**args)
::IncidentManagement::OncallSchedulesFinder.new(context[:current_user], project).execute
end
end
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
class OncallScheduleType < BaseObject
graphql_name 'IncidentManagementOncallSchedule'
description 'Describes an incident management on-call schedule'
authorize :read_incident_management_oncall_schedule
field :iid,
GraphQL::ID_TYPE,
null: false,
description: 'Internal ID of the on-call schedule'
field :name,
GraphQL::STRING_TYPE,
null: false,
description: 'Name of the on-call schedule'
field :description,
GraphQL::STRING_TYPE,
null: true,
description: 'Description of the on-call schedule'
field :timezone,
GraphQL::STRING_TYPE,
null: false,
description: 'Time zone of the on-call schedule'
end
end
end
...@@ -98,6 +98,8 @@ module EE ...@@ -98,6 +98,8 @@ module EE
has_many :sourced_pipelines, class_name: 'Ci::Sources::Project', foreign_key: :source_project_id has_many :sourced_pipelines, class_name: 'Ci::Sources::Project', foreign_key: :source_project_id
has_many :incident_management_oncall_schedules, class_name: 'IncidentManagement::OncallSchedule', inverse_of: :project
scope :with_shared_runners_limit_enabled, -> do scope :with_shared_runners_limit_enabled, -> do
if ::Ci::Runner.has_shared_runners_with_non_zero_public_cost? if ::Ci::Runner.has_shared_runners_with_non_zero_public_cost?
with_shared_runners with_shared_runners
......
# frozen_string_literal: true
module IncidentManagement
class OncallSchedule < ApplicationRecord
self.table_name = 'incident_management_oncall_schedules'
include IidRoutes
include AtomicInternalId
NAME_LENGTH = 200
DESCRIPTION_LENGTH = 1000
belongs_to :project, inverse_of: :incident_management_oncall_schedules
has_internal_id :iid, scope: :project
validates :name, presence: true, uniqueness: { scope: :project }, length: { maximum: NAME_LENGTH }
validates :description, length: { maximum: DESCRIPTION_LENGTH }
validates :timezone, presence: true, inclusion: { in: :timezones }
private
def timezones
@timezones ||= ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier }
end
end
end
...@@ -127,6 +127,7 @@ class License < ApplicationRecord ...@@ -127,6 +127,7 @@ class License < ApplicationRecord
unprotection_restrictions unprotection_restrictions
ci_project_subscriptions ci_project_subscriptions
incident_timeline_view incident_timeline_view
oncall_schedules
] ]
EEP_FEATURES.freeze EEP_FEATURES.freeze
......
...@@ -178,6 +178,7 @@ module EE ...@@ -178,6 +178,7 @@ module EE
enable :read_deploy_board enable :read_deploy_board
enable :admin_epic_issue enable :admin_epic_issue
enable :read_group_timelogs enable :read_group_timelogs
enable :read_incident_management_oncall_schedule
end end
rule { can?(:developer_access) }.policy do rule { can?(:developer_access) }.policy do
...@@ -241,6 +242,7 @@ module EE ...@@ -241,6 +242,7 @@ module EE
enable :modify_auto_fix_setting enable :modify_auto_fix_setting
enable :modify_merge_request_author_setting enable :modify_merge_request_author_setting
enable :modify_merge_request_committer_setting enable :modify_merge_request_committer_setting
enable :admin_incident_management_oncall_schedule
end end
rule { license_scanning_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy rule { license_scanning_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy
......
# frozen_string_literal: true
module IncidentManagement
class OncallSchedulePolicy < ::BasePolicy
delegate { @subject.project }
end
end
# frozen_string_literal: true
module IncidentManagement
module OncallSchedules
class CreateService
# @param project [Project]
# @param user [User]
# @param params [Hash]
def initialize(project, user, params)
@project = project
@user = user
@params = params
end
def execute
return error_no_license unless available?
return error_no_permissions unless allowed?
oncall_schedule = project.incident_management_oncall_schedules.create(params)
return error_in_create(oncall_schedule) unless oncall_schedule.persisted?
success(oncall_schedule)
end
private
attr_reader :project, :user, :params
def allowed?
user&.can?(:admin_incident_management_oncall_schedule, project)
end
def available?
Feature.enabled?(:oncall_schedules_mvc, project) &&
project.feature_available?(:oncall_schedules)
end
def error(message)
ServiceResponse.error(message: message)
end
def success(oncall_schedule)
ServiceResponse.success(payload: { oncall_schedule: oncall_schedule })
end
def error_no_permissions
error(_('You have insufficient permissions to create an on-call schedule for this project'))
end
def error_no_license
error(_('Your license does not support on-call schedules'))
end
def error_in_create(oncall_schedule)
error(oncall_schedule.errors.full_messages.to_sentence)
end
end
end
end
---
name: oncall_schedules_mvc
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47407
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/283914
milestone: '13.7'
type: development
group: group::monitor
default_enabled: false
# frozen_string_literal: true
FactoryBot.define do
factory :incident_management_oncall_schedule, class: 'IncidentManagement::OncallSchedule' do
project
name { 'Default On-call Schedule' }
description { 'On-call description' }
timezone { 'Europe/Berlin' }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedulesFinder do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be(:another_oncall_schedule) { create(:incident_management_oncall_schedule) }
describe '#execute' do
subject(:execute) { described_class.new(current_user, project).execute }
context 'when feature is available' do
before do
stub_licensed_features(oncall_schedules: true)
end
context 'when user has permissions' do
before do
project.add_maintainer(current_user)
end
it 'returns project on-call schedules' do
is_expected.to contain_exactly(oncall_schedule)
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to eq(IncidentManagement::OncallSchedule.none) }
end
end
context 'when user has no permissions' do
it { is_expected.to eq(IncidentManagement::OncallSchedule.none) }
end
end
context 'when feature is not avaiable' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to eq(IncidentManagement::OncallSchedule.none) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::OncallSchedule::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:args) do
{
project_path: project.full_path,
name: 'On-call schedule',
description: 'On-call schedule description',
timezone: 'Europe/Berlin'
}
end
specify { expect(described_class).to require_graphql_authorizations(:admin_incident_management_oncall_schedule) }
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
context 'user has access to project' do
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(current_user)
end
context 'when OncallSchedules::CreateService responds with success' do
it 'returns the on-call schedule with no errors' do
expect(resolve).to eq(
oncall_schedule: ::IncidentManagement::OncallSchedule.last,
errors: []
)
end
end
context 'when OncallSchedules::CreateService responds with an error' do
before do
allow_any_instance_of(::IncidentManagement::OncallSchedules::CreateService)
.to receive(:execute)
.and_return(ServiceResponse.error(payload: { oncall_schedule: nil }, message: 'An on-call schedule already exists'))
end
it 'returns errors' do
expect(resolve).to eq(
oncall_schedule: nil,
errors: ['An on-call schedule already exists']
)
end
end
end
context 'when resource is not accessible to the user' do
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
private
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::IncidentManagement::OncallScheduleResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
subject { sync(resolve_oncall_schedules) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(current_user)
end
specify do
expect(described_class).to have_nullable_graphql_type(Types::IncidentManagement::OncallScheduleType.connection_type)
end
it 'returns on-call schedules' do
is_expected.to contain_exactly(oncall_schedule)
end
private
def resolve_oncall_schedules(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['IncidentManagementOncallSchedule'] do
specify { expect(described_class.graphql_name).to eq('IncidentManagementOncallSchedule') }
specify { expect(described_class).to require_graphql_authorizations(:read_incident_management_oncall_schedule) }
it 'exposes the expected fields' do
expected_fields = %i[
iid
name
description
timezone
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedule do
let_it_be(:project) { create(:project) }
describe '.associations' do
it { is_expected.to belong_to(:project) }
end
describe '.validations' do
let(:timezones) { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier } }
subject { build(:incident_management_oncall_schedule, project: project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(200) }
it { is_expected.to validate_length_of(:description).is_at_most(1000) }
it { is_expected.to validate_presence_of(:timezone) }
it { is_expected.to validate_inclusion_of(:timezone).in_array(timezones) }
context 'when the oncall schedule with the same name exists' do
before do
create(:incident_management_oncall_schedule, project: project)
end
it 'has validation errors' do
expect(subject).to be_invalid
expect(subject.errors.full_messages.to_sentence).to eq('Name has already been taken')
end
end
end
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:incident_management_oncall_schedule) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :incident_management_oncall_schedules }
end
end
...@@ -51,6 +51,8 @@ RSpec.describe Project do ...@@ -51,6 +51,8 @@ RSpec.describe Project do
it { is_expected.to have_many(:project_aliases) } it { is_expected.to have_many(:project_aliases) }
it { is_expected.to have_many(:approval_rules) } it { is_expected.to have_many(:approval_rules) }
it { is_expected.to have_many(:incident_management_oncall_schedules).class_name('IncidentManagement::OncallSchedule') }
describe 'approval_rules association' do describe 'approval_rules association' do
let_it_be(:rule, reload: true) { create(:approval_project_rule) } let_it_be(:rule, reload: true) { create(:approval_project_rule) }
let(:project) { rule.project } let(:project) { rule.project }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedulePolicy do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
subject(:policy) { described_class.new(user, oncall_schedule) }
describe 'rules' do
it { is_expected.to be_disallowed :read_incident_management_oncall_schedule }
context 'when maintainer' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_allowed :read_incident_management_oncall_schedule }
end
end
end
...@@ -1342,6 +1342,58 @@ RSpec.describe ProjectPolicy do ...@@ -1342,6 +1342,58 @@ RSpec.describe ProjectPolicy do
end end
end end
describe 'Incident Management on-call schedules' do
using RSpec::Parameterized::TableSyntax
context ':read_incident_management_oncall_schedule' do
let(:policy) { :read_incident_management_oncall_schedule }
where(:role, :admin_mode, :allowed) do
:guest | nil | false
:reporter | nil | true
:developer | nil | true
:maintainer | nil | true
:owner | nil | true
:admin | false | false
:admin | true | true
end
before do
enable_admin_mode!(current_user) if admin_mode
end
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
context ':admin_incident_management_oncall_schedule' do
let(:policy) { :admin_incident_management_oncall_schedule }
where(:role, :admin_mode, :allowed) do
:guest | nil | false
:reporter | nil | false
:developer | nil | false
:maintainer | nil | true
:owner | nil | true
:admin | false | false
:admin | true | true
end
before do
enable_admin_mode!(current_user) if admin_mode
end
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
end
context 'when project is readonly because the storage usage limit has been exceeded on the root namespace' do context 'when project is readonly because the storage usage limit has been exceeded on the root namespace' do
let(:current_user) { owner } let(:current_user) { owner }
let(:abilities) do let(:abilities) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Creating a new on-call schedule' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:variables) do
{
project_path: project.full_path,
name: 'New on-call schedule',
description: 'on-call schedule description',
timezone: 'Europe/Berlin'
}
end
let(:mutation) do
graphql_mutation(:oncall_schedule_create, variables) do
<<~QL
clientMutationId
errors
oncallSchedule {
iid
name
description
timezone
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:oncall_schedule_create) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(current_user)
end
it 'create a new on-call schedule' do
post_graphql_mutation(mutation, current_user: current_user)
new_oncall_schedule = ::IncidentManagement::OncallSchedule.last!
oncall_schedule_response = mutation_response['oncallSchedule']
expect(response).to have_gitlab_http_status(:success)
expect(oncall_schedule_response.slice(*%w[iid name description timezone])).to eq(
'iid' => new_oncall_schedule.iid.to_s,
'name' => 'New on-call schedule',
'description' => 'on-call schedule description',
'timezone' => 'Europe/Berlin'
)
end
%i[project_path name timezone].each do |argument|
context "without required argument #{argument}" do
before do
variables.delete(argument)
end
it_behaves_like 'an invalid argument to the mutation', argument_name: argument
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting Incident Management on-call schedules' do
include GraphqlHelpers
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('IncidentManagementOncallSchedule')}
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('incidentManagementOncallSchedules', {}, fields)
)
end
let(:oncall_schedules) { graphql_data.dig('project', 'incidentManagementOncallSchedules', 'nodes') }
before do
stub_licensed_features(oncall_schedules: true)
end
context 'without project permissions' do
let(:user) { create(:user) }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(oncall_schedules).to be_nil }
end
context 'with project permissions' do
before do
project.add_maintainer(current_user)
end
context 'with unavailable feature' do
before do
stub_licensed_features(oncall_schedules: false)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(oncall_schedules).to be_empty }
end
context 'without on-call schedules' do
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(oncall_schedules).to be_empty }
end
context 'with on-call schedules' do
let!(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:last_oncall_schedule) { oncall_schedules.last }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'returns the correct properties of the on-call schedule' do
expect(last_oncall_schedule).to include(
'iid' => oncall_schedule.iid.to_s,
'name' => oncall_schedule.name,
'description' => oncall_schedule.description,
'timezone' => oncall_schedule.timezone
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallSchedules::CreateService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be_with_refind(:project) { create(:project) }
let(:current_user) { user_with_permissions }
let(:params) { { name: 'On-call schedule', description: 'On-call schedule description', timezone: 'Europe/Berlin' } }
let(:service) { described_class.new(project, current_user, params) }
before do
stub_licensed_features(oncall_schedules: true)
project.add_maintainer(user_with_permissions)
end
describe '#execute' do
shared_examples 'error response' do |message|
it 'has an informative message' do
expect(execute).to be_error
expect(execute.message).to eq(message)
end
end
subject(:execute) { service.execute }
context 'when the current_user is anonymous' do
let(:current_user) { nil }
it_behaves_like 'error response', 'You have insufficient permissions to create an on-call schedule for this project'
end
context 'when the current_user does not have permissions to create on-call schedules' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to create an on-call schedule for this project'
end
context 'when feature is not available' do
before do
stub_licensed_features(oncall_schedules: false)
end
it_behaves_like 'error response', 'Your license does not support on-call schedules'
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it_behaves_like 'error response', 'Your license does not support on-call schedules'
end
context 'when an on-call schedule already exists' do
let!(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project, name: 'On-call schedule') }
it_behaves_like 'error response', 'Name has already been taken'
end
context 'with valid params' do
it 'successfully creates an on-call schedule' do
expect(execute).to be_success
oncall_schedule = execute.payload[:oncall_schedule]
expect(oncall_schedule).to be_a(::IncidentManagement::OncallSchedule)
expect(oncall_schedule.name).to eq('On-call schedule')
expect(oncall_schedule.description).to eq('On-call schedule description')
expect(oncall_schedule.timezone).to eq('Europe/Berlin')
end
end
end
end
...@@ -31265,6 +31265,9 @@ msgstr "" ...@@ -31265,6 +31265,9 @@ msgstr ""
msgid "You have insufficient permissions to create an HTTP integration for this project" msgid "You have insufficient permissions to create an HTTP integration for this project"
msgstr "" msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr ""
msgid "You have insufficient permissions to remove this HTTP integration" msgid "You have insufficient permissions to remove this HTTP integration"
msgstr "" msgstr ""
...@@ -31631,6 +31634,9 @@ msgstr "" ...@@ -31631,6 +31634,9 @@ msgstr ""
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email." msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
msgstr "" msgstr ""
msgid "Your license does not support on-call schedules"
msgstr ""
msgid "Your license is valid from" msgid "Your license is valid from"
msgstr "" msgstr ""
......
...@@ -554,6 +554,7 @@ project: ...@@ -554,6 +554,7 @@ project:
- terraform_states - terraform_states
- alert_management_http_integrations - alert_management_http_integrations
- exported_protected_branches - exported_protected_branches
- incident_management_oncall_schedules
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
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