Commit d2dcd975 authored by James Lopez's avatar James Lopez

Merge branch '211461-create-annotations-graphql-endpoint' into 'master'

Create dashboard annotations via Graphql API

See merge request gitlab-org/gitlab!31249
parents 4b188141 11eeba43
# frozen_string_literal: true
module Mutations
module Metrics
module Dashboard
module Annotations
class Create < BaseMutation
graphql_name 'CreateAnnotation'
ANNOTATION_SOURCE_ARGUMENT_ERROR = 'Either a cluster or environment global id is required'
INVALID_ANNOTATION_SOURCE_ERROR = 'Invalid cluster or environment id'
authorize :create_metrics_dashboard_annotation
field :annotation,
Types::Metrics::Dashboards::AnnotationType,
null: true,
description: 'The created annotation'
argument :environment_id,
GraphQL::ID_TYPE,
required: false,
description: 'The global id of the environment to add an annotation to'
argument :cluster_id,
GraphQL::ID_TYPE,
required: false,
description: 'The global id of the cluster to add an annotation to'
argument :starting_at, Types::TimeType,
required: true,
description: 'Timestamp indicating starting moment to which the annotation relates'
argument :ending_at, Types::TimeType,
required: false,
description: 'Timestamp indicating ending moment to which the annotation relates'
argument :dashboard_path,
GraphQL::STRING_TYPE,
required: true,
description: 'The path to a file defining the dashboard on which the annotation should be added'
argument :description,
GraphQL::STRING_TYPE,
required: true,
description: 'The description of the annotation'
AnnotationSource = Struct.new(:object, keyword_init: true) do
def type_keys
{ 'Clusters::Cluster' => :cluster, 'Environment' => :environment }
end
def klass
object.class.name
end
def type
raise Gitlab::Graphql::Errors::ArgumentError, INVALID_ANNOTATION_SOURCE_ERROR unless type_keys[klass]
type_keys[klass]
end
end
def resolve(args)
annotation_response = ::Metrics::Dashboard::Annotations::CreateService.new(context[:current_user], annotation_create_params(args)).execute
annotation = annotation_response[:annotation]
{
annotation: annotation.valid? ? annotation : nil,
errors: errors_on_object(annotation)
}
end
private
def ready?(**args)
# Raise error if both cluster_id and environment_id are present or neither is present
unless args[:cluster_id].present? ^ args[:environment_id].present?
raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR
end
super(args)
end
def find_object(id:)
GitlabSchema.object_from_id(id)
end
def annotation_create_params(args)
annotation_source = AnnotationSource.new(object: annotation_source(args))
args[annotation_source.type] = annotation_source.object
args
end
def annotation_source(args)
annotation_source_id = args[:cluster_id] || args[:environment_id]
authorized_find!(id: annotation_source_id)
end
end
end
end
end
end
...@@ -22,6 +22,7 @@ module Types ...@@ -22,6 +22,7 @@ module Types
mount_mutation Mutations::MergeRequests::SetSubscription mount_mutation Mutations::MergeRequests::SetSubscription
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
......
---
title: Create dashboard annotations via Graphql
merge_request: 31249
author:
type: added
...@@ -945,6 +945,66 @@ type CreateAlertIssuePayload { ...@@ -945,6 +945,66 @@ type CreateAlertIssuePayload {
issue: Issue issue: Issue
} }
"""
Autogenerated input type of CreateAnnotation
"""
input CreateAnnotationInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the cluster to add an annotation to
"""
clusterId: ID
"""
The path to a file defining the dashboard on which the annotation should be added
"""
dashboardPath: String!
"""
The description of the annotation
"""
description: String!
"""
Timestamp indicating ending moment to which the annotation relates
"""
endingAt: Time
"""
The global id of the environment to add an annotation to
"""
environmentId: ID
"""
Timestamp indicating starting moment to which the annotation relates
"""
startingAt: Time!
}
"""
Autogenerated return type of CreateAnnotation
"""
type CreateAnnotationPayload {
"""
The created annotation
"""
annotation: MetricsDashboardAnnotation
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
""" """
Autogenerated input type of CreateBranch Autogenerated input type of CreateBranch
""" """
...@@ -6558,6 +6618,7 @@ type Mutation { ...@@ -6558,6 +6618,7 @@ type Mutation {
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload
createAnnotation(input: CreateAnnotationInput!): CreateAnnotationPayload
createBranch(input: CreateBranchInput!): CreateBranchPayload createBranch(input: CreateBranchInput!): CreateBranchPayload
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
createEpic(input: CreateEpicInput!): CreateEpicPayload createEpic(input: CreateEpicInput!): CreateEpicPayload
......
...@@ -2548,6 +2548,166 @@ ...@@ -2548,6 +2548,166 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "CreateAnnotationInput",
"description": "Autogenerated input type of CreateAnnotation",
"fields": null,
"inputFields": [
{
"name": "environmentId",
"description": "The global id of the environment to add an annotation to",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "clusterId",
"description": "The global id of the cluster to add an annotation to",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "startingAt",
"description": "Timestamp indicating starting moment to which the annotation relates",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "endingAt",
"description": "Timestamp indicating ending moment to which the annotation relates",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "dashboardPath",
"description": "The path to a file defining the dashboard on which the annotation should be added",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "description",
"description": "The description of the annotation",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CreateAnnotationPayload",
"description": "Autogenerated return type of CreateAnnotation",
"fields": [
{
"name": "annotation",
"description": "The created annotation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "MetricsDashboardAnnotation",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "CreateBranchInput", "name": "CreateBranchInput",
...@@ -18569,6 +18729,33 @@ ...@@ -18569,6 +18729,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "createAnnotation",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CreateAnnotationInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "CreateAnnotationPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "createBranch", "name": "createBranch",
"description": null, "description": null,
...@@ -175,6 +175,16 @@ Autogenerated return type of CreateAlertIssue ...@@ -175,6 +175,16 @@ Autogenerated return type of CreateAlertIssue
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue created after mutation | | `issue` | Issue | The issue created after mutation |
## CreateAnnotationPayload
Autogenerated return type of CreateAnnotation
| Name | Type | Description |
| --- | ---- | ---------- |
| `annotation` | MetricsDashboardAnnotation | The created annotation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## CreateBranchPayload ## CreateBranchPayload
Autogenerated return type of CreateBranch Autogenerated return type of CreateBranch
......
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Metrics::Dashboard::Annotations::Create do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project, :private, :repository) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:cluster) { create(:cluster, projects: [project]) }
let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
let(:starting_at) { Time.current.iso8601 }
let(:ending_at) { 1.hour.from_now.iso8601 }
let(:description) { 'test description' }
def mutation_response
graphql_mutation_response(:create_annotation)
end
specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) }
context 'when annotation source is environment' do
let(:mutation) do
variables = {
environment_id: GitlabSchema.id_from_object(environment).to_s,
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
context 'when the user does not have permission' do
before do
project.add_reporter(current_user)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not create the annotation' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { Metrics::Dashboard::Annotation.count }
end
end
context 'when the user has permission' do
before do
project.add_developer(current_user)
end
it 'creates the annotation' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { Metrics::Dashboard::Annotation.count }.by(1)
end
it 'returns the created annotation' do
post_graphql_mutation(mutation, current_user: current_user)
annotation = Metrics::Dashboard::Annotation.first
annotation_id = GitlabSchema.id_from_object(annotation).to_s
expect(mutation_response['annotation']['description']).to match(description)
expect(mutation_response['annotation']['startingAt'].to_time).to match(starting_at.to_time)
expect(mutation_response['annotation']['endingAt'].to_time).to match(ending_at.to_time)
expect(mutation_response['annotation']['id']).to match(annotation_id)
expect(annotation.environment_id).to eq(environment.id)
end
context 'when environment_id is missing' do
let(:mutation) do
variables = {
environment_id: nil,
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
end
context 'when environment_id is invalid' do
let(:mutation) do
variables = {
environment_id: 'invalid_id',
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
end
end
end
context 'when annotation source is cluster' do
let(:mutation) do
variables = {
cluster_id: GitlabSchema.id_from_object(cluster).to_s,
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
context 'with permission' do
before do
project.add_developer(current_user)
end
it 'creates the annotation' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { Metrics::Dashboard::Annotation.count }.by(1)
end
it 'returns the created annotation' do
post_graphql_mutation(mutation, current_user: current_user)
annotation = Metrics::Dashboard::Annotation.first
annotation_id = GitlabSchema.id_from_object(annotation).to_s
expect(mutation_response['annotation']['description']).to match(description)
expect(mutation_response['annotation']['startingAt'].to_time).to match(starting_at.to_time)
expect(mutation_response['annotation']['endingAt'].to_time).to match(ending_at.to_time)
expect(mutation_response['annotation']['id']).to match(annotation_id)
expect(annotation.cluster_id).to eq(cluster.id)
end
context 'when cluster_id is missing' do
let(:mutation) do
variables = {
cluster_id: nil,
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
end
end
context 'without permission' do
before do
project.add_guest(current_user)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not create the annotation' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { Metrics::Dashboard::Annotation.count }
end
end
context 'when cluster_id is invalid' do
let(:mutation) do
variables = {
cluster_id: 'invalid_id',
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
end
end
context 'when both environment_id and cluster_id are provided' do
let(:mutation) do
variables = {
environment_id: GitlabSchema.id_from_object(environment).to_s,
cluster_id: GitlabSchema.id_from_object(cluster).to_s,
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
end
context 'when a non-cluster or environment id is provided' do
let(:mutation) do
variables = {
environment_id: GitlabSchema.id_from_object(project).to_s,
starting_at: starting_at,
ending_at: ending_at,
dashboard_path: dashboard_path,
description: description
}
graphql_mutation(:create_annotation, variables)
end
before do
project.add_developer(current_user)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::INVALID_ANNOTATION_SOURCE_ERROR]
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment