Commit 11eeba43 authored by Kirstie Cook's avatar Kirstie Cook Committed by James Lopez

Add annotations create endpoint for GraphQL

Add specs for creating annotations

Update graphql docs

Update graphql schema

Clean up specs

Add changelog entry

Update graphql docs
parent c91c4eae
# 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
mount_mutation Mutations::MergeRequests::SetSubscription
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
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::DiffNote, 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 {
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
"""
......@@ -6558,6 +6618,7 @@ type Mutation {
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload
createAnnotation(input: CreateAnnotationInput!): CreateAnnotationPayload
createBranch(input: CreateBranchInput!): CreateBranchPayload
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
createEpic(input: CreateEpicInput!): CreateEpicPayload
......
......@@ -2548,6 +2548,166 @@
"enumValues": 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",
"name": "CreateBranchInput",
......@@ -18569,6 +18729,33 @@
"isDeprecated": false,
"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",
"description": null,
......@@ -175,6 +175,16 @@ Autogenerated return type of CreateAlertIssue
| `errors` | String! => Array | Errors encountered during execution of the 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
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