Skip to content
Snippets Groups Projects
Commit b8b97284 authored by Dylan Griffith's avatar Dylan Griffith
Browse files

Merge branch '321087-adapt-devops-adoption-for-groups' into 'master'

Introduce group-level API for devops adoption

See merge request gitlab-org/gitlab!55479
parents 2f6d081d 893e6633
No related branches found
No related tags found
No related merge requests found
Showing
with 259 additions and 201 deletions
Loading
Loading
@@ -86,8 +86,10 @@ Returns [`DevopsAdoptionSegmentConnection`](#devopsadoptionsegmentconnection).
| ---- | ---- | ----------- |
| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
| `directDescendantsOnly` | [`Boolean`](#boolean) | Limits segments to direct descendants of specified parent. |
| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
| `parentNamespaceId` | [`NamespaceID`](#namespaceid) | Filter by ancestor namespace. |
 
### `echo`
 
Loading
Loading
# frozen_string_literal: true
module Analytics
module DevopsAdoption
class SegmentsFinder
attr_reader :params, :current_user
def initialize(current_user, params:)
@current_user = current_user
@params = params
end
def execute
scope = ::Analytics::DevopsAdoption::Segment.ordered_by_name
if direct_descendants_only?
scope = scope.for_namespaces(parent_with_direct_descendants)
else
scope = scope.for_parent(parent_namespace) if parent_namespace
end
scope
end
private
def parent_with_direct_descendants
parent_namespace ? [parent_namespace] + parent_namespace.children : ::Group.top_most
end
def parent_namespace
params[:parent_namespace]
end
def direct_descendants_only?
params[:direct_descendants_only]
end
end
end
end
Loading
Loading
@@ -60,9 +60,9 @@ module MutationType
mount_mutation ::Mutations::DastSiteTokens::Create
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
mount_mutation ::Mutations::QualityManagement::TestCases::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
Loading
Loading
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
graphql_name 'CreateDevopsAdoptionSegment'
argument :namespace_id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'Namespace ID to set for the segment.'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation.'
def resolve(namespace_id:, **)
namespace = namespace_id.find
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
response = service.execute
resolve_segment(response)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class Delete < BaseMutation
include Mixins::CommonMethods
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: "One or many IDs of the segments to delete."
def resolve(id:, **)
segments = GlobalID::Locator.locate_many(id)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
module Mixins
# This module ensures that the mutations are admin only
module CommonMethods
ADMIN_MESSAGE = 'You must be an admin to use this mutation'
FEATURE_UNAVAILABLE_MESSAGE = 'Feature is not available'
def ready?(**args)
unless License.feature_available?(:instance_level_devops_adoption)
raise_resource_not_available_error!(FEATURE_UNAVAILABLE_MESSAGE)
end
super
end
private
def resolve_segment(response)
segment = response.payload.fetch(:segment)
{
segment: response.success? ? response.payload.fetch(:segment) : nil,
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!(ADMIN_MESSAGE)
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class Create < BaseMutation
include Mixins::CommonMethods
graphql_name 'CreateDevopsAdoptionSegment'
argument :namespace_id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'Namespace ID to set for the segment.'
field :segment,
Types::Admin::Analytics::DevopsAdoption::SegmentType,
null: true,
description: 'The segment after mutation.'
def resolve(namespace_id:, **)
namespace = namespace_id.find
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
response = service.execute
resolve_segment(response)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
class Delete < BaseMutation
include Mixins::CommonMethods
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: 'One or many IDs of the segments to delete.'
def resolve(id:, **)
segments = GlobalID::Locator.locate_many(id)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Analytics
module DevopsAdoption
module Segments
module Mixins
module CommonMethods
private
def resolve_segment(response)
segment = response.payload.fetch(:segment)
{
segment: response.success? ? response.payload.fetch(:segment) : nil,
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!
end
end
end
end
end
end
end
Loading
Loading
@@ -6,31 +6,44 @@ module Analytics
module DevopsAdoption
class SegmentsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include Gitlab::Allowable
 
type Types::Admin::Analytics::DevopsAdoption::SegmentType, null: true
 
def resolve
authorize!
argument :parent_namespace_id, ::Types::GlobalIDType[::Namespace],
required: false,
description: 'Filter by ancestor namespace.'
 
if segments_feature_available?
::Analytics::DevopsAdoption::Segment.ordered_by_name
else
::Analytics::DevopsAdoption::Segment.none
end
argument :direct_descendants_only, ::GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Limits segments to direct descendants of specified parent.'
def resolve(parent_namespace_id: nil, direct_descendants_only: false, **)
parent = GlobalID::Locator.locate(parent_namespace_id) if parent_namespace_id
authorize!(parent)
::Analytics::DevopsAdoption::SegmentsFinder.new(current_user, params: {
parent_namespace: parent, direct_descendants_only: direct_descendants_only
}).execute
end
 
private
 
def segments_feature_available?
License.feature_available?(:instance_level_devops_adoption)
def authorize!(parent)
parent ? authorize_with_namespace!(parent) : authorize_global!
end
 
def authorize!
admin? || raise_resource_not_available_error!
def authorize_global!
unless can?(current_user, :view_instance_devops_adoption)
raise_resource_not_available_error!
end
end
 
def admin?
context[:current_user].present? && context[:current_user].admin?
def authorize_with_namespace!(parent)
unless can?(current_user, :view_group_devops_adoption, parent)
raise_resource_not_available_error!
end
end
end
end
Loading
Loading
Loading
Loading
@@ -12,4 +12,13 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
validates :namespace, uniqueness: true, presence: true
 
scope :ordered_by_name, -> { includes(:namespace).order('"namespaces"."name" ASC') }
scope :for_namespaces, -> (namespaces) { where(namespace_id: namespaces) }
scope :for_parent, -> (namespace) {
if Feature.enabled?(:recursive_namespace_lookup_as_inner_join, namespace)
join_sql = namespace.self_and_descendants.to_sql
joins("INNER JOIN (#{join_sql}) namespaces ON namespaces.id=#{self.arel_table.name}.namespace_id")
else
for_namespaces(namespace.self_and_descendants)
end
}
end
Loading
Loading
@@ -27,6 +27,7 @@ class License < ApplicationRecord
group_webhooks
group_level_devops_adoption
instance_level_devops_adoption
group_level_devops_adoption
issuable_default_templates
issue_weights
iterations
Loading
Loading
Loading
Loading
@@ -29,13 +29,21 @@ module GlobalPolicy
end
end
 
condition(:instance_devops_adoption_available) do
::License.feature_available?(:instance_level_devops_adoption)
end
rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
 
rule { admin & instance_devops_adoption_available }.policy do
enable :manage_devops_adoption_segments
enable :view_instance_devops_adoption
end
rule { admin }.policy do
enable :read_licenses
enable :destroy_licenses
enable :read_all_geo
enable :manage_devops_adoption_segments
enable :manage_subscription
end
 
Loading
Loading
Loading
Loading
@@ -197,6 +197,7 @@ module GroupPolicy
end
 
rule { reporter & group_devops_adoption_available }.policy do
enable :manage_devops_adoption_segments
enable :view_group_devops_adoption
end
 
Loading
Loading
Loading
Loading
@@ -4,21 +4,19 @@ module Analytics
module DevopsAdoption
module Segments
class BulkDeleteService
include CommonMethods
def initialize(segments:, current_user:)
@segments = segments
@current_user = current_user
end
 
def execute
authorize!
deletion_services.map(&:authorize!)
 
result = nil
 
ActiveRecord::Base.transaction do
segments.each do |segment|
response = delete_segment(segment)
deletion_services.each do |service|
response = service.execute
 
if response.error?
result = ServiceResponse.error(message: response.message, payload: response_payload)
Loading
Loading
@@ -40,8 +38,10 @@ def response_payload
{ segments: segments }
end
 
def delete_segment(segment)
DeleteService.new(current_user: current_user, segment: segment).execute
def deletion_services
@deletion_services ||= segments.map do |segment|
DeleteService.new(current_user: current_user, segment: segment)
end
end
end
end
Loading
Loading
Loading
Loading
@@ -4,8 +4,6 @@ module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
Loading
Loading
@@ -14,20 +12,26 @@ def initialize(params: {}, current_user:)
def execute
authorize!
 
segments = params[:namespaces].map do |namespace|
response = FindOrCreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
response.payload[:segment]
segments = services.map do |service|
service.execute.payload[:segment]
end
 
ServiceResponse.success(payload: { segments: segments })
end
 
def authorize!
services.each(&:authorize!)
end
private
 
attr_reader :params, :current_user
def services
@services ||= params[:namespaces].map do |namespace|
FindOrCreateService.new(current_user: current_user, params: { namespace: namespace })
end
end
end
end
end
Loading
Loading
Loading
Loading
@@ -7,7 +7,7 @@ module CommonMethods
include Gitlab::Allowable
 
def authorize!
unless can?(current_user, :manage_devops_adoption_segments, :global)
unless can?(current_user, :manage_devops_adoption_segments, namespace)
raise AuthorizationError.new(self, 'Forbidden')
end
end
Loading
Loading
Loading
Loading
@@ -15,7 +15,7 @@ def initialize(segment: Analytics::DevopsAdoption::Segment.new, params: {}, curr
def execute
authorize!
 
segment.assign_attributes(attributes)
segment.assign_attributes(namespace: namespace)
 
if segment.save
Analytics::DevopsAdoption::CreateSnapshotWorker.perform_async(segment.id)
Loading
Loading
@@ -34,8 +34,8 @@ def response_payload
{ segment: segment }
end
 
def attributes
params.slice(:namespace, :namespace_id)
def namespace
params[:namespace]
end
end
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment