Commit 5dd62e36 authored by Doug Stull's avatar Doug Stull

Merge branch '332069-ci-usage-graphql' into 'master'

Add CI minutes usage data to GraphQL

See merge request gitlab-org/gitlab!66635
parents 815f9988 1baafd72
...@@ -54,6 +54,16 @@ Returns [`CiConfig`](#ciconfig). ...@@ -54,6 +54,16 @@ Returns [`CiConfig`](#ciconfig).
| <a id="queryciconfigprojectpath"></a>`projectPath` | [`ID!`](#id) | The project of the CI config. | | <a id="queryciconfigprojectpath"></a>`projectPath` | [`ID!`](#id) | The project of the CI config. |
| <a id="queryciconfigsha"></a>`sha` | [`String`](#string) | Sha for the pipeline. | | <a id="queryciconfigsha"></a>`sha` | [`String`](#string) | Sha for the pipeline. |
### `Query.ciMinutesUsage`
The monthly CI minutes usage data for the current user.
Returns [`CiMinutesNamespaceMonthlyUsageConnection`](#ciminutesnamespacemonthlyusageconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
### `Query.containerRepository` ### `Query.containerRepository`
Find a container repository. Find a container repository.
...@@ -4876,6 +4886,52 @@ The edge type for [`CiJob`](#cijob). ...@@ -4876,6 +4886,52 @@ The edge type for [`CiJob`](#cijob).
| <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. | | <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. |
#### `CiMinutesNamespaceMonthlyUsageConnection`
The connection type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="ciminutesnamespacemonthlyusageconnectionedges"></a>`edges` | [`[CiMinutesNamespaceMonthlyUsageEdge]`](#ciminutesnamespacemonthlyusageedge) | A list of edges. |
| <a id="ciminutesnamespacemonthlyusageconnectionnodes"></a>`nodes` | [`[CiMinutesNamespaceMonthlyUsage]`](#ciminutesnamespacemonthlyusage) | A list of nodes. |
| <a id="ciminutesnamespacemonthlyusageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `CiMinutesNamespaceMonthlyUsageEdge`
The edge type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="ciminutesnamespacemonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="ciminutesnamespacemonthlyusageedgenode"></a>`node` | [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage) | The item at the end of the edge. |
#### `CiMinutesProjectMonthlyUsageConnection`
The connection type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="ciminutesprojectmonthlyusageconnectionedges"></a>`edges` | [`[CiMinutesProjectMonthlyUsageEdge]`](#ciminutesprojectmonthlyusageedge) | A list of edges. |
| <a id="ciminutesprojectmonthlyusageconnectionnodes"></a>`nodes` | [`[CiMinutesProjectMonthlyUsage]`](#ciminutesprojectmonthlyusage) | A list of nodes. |
| <a id="ciminutesprojectmonthlyusageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `CiMinutesProjectMonthlyUsageEdge`
The edge type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="ciminutesprojectmonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="ciminutesprojectmonthlyusageedgenode"></a>`node` | [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage) | The item at the end of the edge. |
#### `CiRunnerConnection` #### `CiRunnerConnection`
The connection type for [`CiRunner`](#cirunner). The connection type for [`CiRunner`](#cirunner).
...@@ -7858,6 +7914,25 @@ Represents the total number of issues and their weights for a particular day. ...@@ -7858,6 +7914,25 @@ Represents the total number of issues and their weights for a particular day.
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="cijobtokenscopetypeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) | | <a id="cijobtokenscopetypeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) |
### `CiMinutesNamespaceMonthlyUsage`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="ciminutesnamespacemonthlyusageminutes"></a>`minutes` | [`Int`](#int) | The total number of minutes used by all projects in the namespace. |
| <a id="ciminutesnamespacemonthlyusagemonth"></a>`month` | [`String`](#string) | The month related to the usage data. |
| <a id="ciminutesnamespacemonthlyusageprojects"></a>`projects` | [`CiMinutesProjectMonthlyUsageConnection`](#ciminutesprojectmonthlyusageconnection) | CI minutes usage data for projects in the namespace. (see [Connections](#connections)) |
### `CiMinutesProjectMonthlyUsage`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="ciminutesprojectmonthlyusageminutes"></a>`minutes` | [`Int`](#int) | The number of CI minutes used by the project in the month. |
| <a id="ciminutesprojectmonthlyusagename"></a>`name` | [`String`](#string) | The name of the project. |
### `CiRunner` ### `CiRunner`
#### Fields #### Fields
......
...@@ -61,6 +61,10 @@ module EE ...@@ -61,6 +61,10 @@ module EE
null: true, null: true,
resolver: ::Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver, resolver: ::Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver,
description: 'Fields related to entries in the license history.' description: 'Fields related to entries in the license history.'
field :ci_minutes_usage, ::Types::Ci::Minutes::NamespaceMonthlyUsageType.connection_type,
null: true,
description: 'The monthly CI minutes usage data for the current user.'
end end
def vulnerability(id:) def vulnerability(id:)
...@@ -76,6 +80,10 @@ module EE ...@@ -76,6 +80,10 @@ module EE
id = ::Types::GlobalIDType[Iteration].coerce_isolated_input(id) id = ::Types::GlobalIDType[Iteration].coerce_isolated_input(id)
::GitlabSchema.find_by_gid(id) ::GitlabSchema.find_by_gid(id)
end end
def ci_minutes_usage
::Ci::Minutes::NamespaceMonthlyUsage.for_namespace(current_user.namespace)
end
end end
end end
end end
# frozen_string_literal: true
module Types
module Ci
module Minutes
# rubocop: disable Graphql/AuthorizeTypes
# this type only exposes data related to the current user
class NamespaceMonthlyUsageType < BaseObject
graphql_name 'CiMinutesNamespaceMonthlyUsage'
field :month, ::GraphQL::STRING_TYPE, null: true,
description: 'The month related to the usage data.'
field :minutes, ::GraphQL::INT_TYPE, null: true,
method: :amount_used,
description: 'The total number of minutes used by all projects in the namespace.'
field :projects, ::Types::Ci::Minutes::ProjectMonthlyUsageType.connection_type, null: true,
description: 'CI minutes usage data for projects in the namespace.'
def month
object.date.strftime('%B')
end
def projects
::Ci::Minutes::ProjectMonthlyUsage.for_namespace_monthly_usage(object)
end
end
end
end
end
# frozen_string_literal: true
module Types
module Ci
module Minutes
# rubocop: disable Graphql/AuthorizeTypes
# this type only exposes data related to the current user
class ProjectMonthlyUsageType < BaseObject
graphql_name 'CiMinutesProjectMonthlyUsage'
field :minutes, ::GraphQL::INT_TYPE, null: true,
method: :amount_used,
description: 'The number of CI minutes used by the project in the month.'
field :name, ::GraphQL::STRING_TYPE, null: true,
description: 'The name of the project.'
def name
object.project.name
end
end
end
end
end
...@@ -10,6 +10,7 @@ module Ci ...@@ -10,6 +10,7 @@ module Ci
belongs_to :namespace belongs_to :namespace
scope :current_month, -> { where(date: beginning_of_month) } scope :current_month, -> { where(date: beginning_of_month) }
scope :for_namespace, -> (namespace) { where(namespace: namespace) }
def self.beginning_of_month(time = Time.current) def self.beginning_of_month(time = Time.current)
time.utc.beginning_of_month time.utc.beginning_of_month
......
...@@ -11,6 +11,13 @@ module Ci ...@@ -11,6 +11,13 @@ module Ci
scope :current_month, -> { where(date: beginning_of_month) } scope :current_month, -> { where(date: beginning_of_month) }
scope :for_namespace_monthly_usage, -> (namespace_monthly_usage) do
where(
date: namespace_monthly_usage.date,
project: namespace_monthly_usage.namespace.projects
)
end
def self.beginning_of_month(time = Time.current) def self.beginning_of_month(time = Time.current)
time.utc.beginning_of_month time.utc.beginning_of_month
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CiMinutesNamespaceMonthlyUsage'] do
it do
expect(described_class).to have_graphql_fields(:minutes, :month, :projects)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CiMinutesProjectMonthlyUsage'] do
it do
expect(described_class).to have_graphql_fields(:minutes, :name)
end
end
...@@ -5,14 +5,15 @@ require 'spec_helper' ...@@ -5,14 +5,15 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Query'] do RSpec.describe GitlabSchema.types['Query'] do
specify do specify do
expect(described_class).to have_graphql_fields( expect(described_class).to have_graphql_fields(
:iteration, :ci_minutes_usage,
:current_license,
:geo_node, :geo_node,
:vulnerabilities,
:vulnerability,
:instance_security_dashboard, :instance_security_dashboard,
:iteration,
:license_history_entries,
:vulnerabilities,
:vulnerabilities_count_by_day, :vulnerabilities_count_by_day,
:current_license, :vulnerability
:license_history_entries
).at_least ).at_least
end end
end end
...@@ -90,4 +90,15 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -90,4 +90,15 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
end end
end end
end end
describe '.for_namespace' do
it 'returns usages for the namespace' do
matching_usage = create(:ci_namespace_monthly_usage, namespace: namespace)
create(:ci_namespace_monthly_usage, namespace: create(:namespace))
usages = described_class.for_namespace(namespace)
expect(usages).to contain_exactly(matching_usage)
end
end
end end
...@@ -90,4 +90,19 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -90,4 +90,19 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
end end
end end
end end
describe '.for_namespace_monthly_usage' do
it "fetches project monthly usages matching the namespace monthly usage's date and namespace" do
date_for_usage = Date.new(2021, 5, 1)
date_not_for_usage = date_for_usage + 1.month
namespace_usage = create(:ci_namespace_monthly_usage, namespace: project.namespace, amount_used: 50, date: date_for_usage)
matching_project_usage = create(:ci_project_monthly_usage, project: project, amount_used: 50, date: date_for_usage)
create(:ci_project_monthly_usage, project: project, amount_used: 50, date: date_not_for_usage)
create(:ci_project_monthly_usage, project: create(:project), amount_used: 50, date: date_for_usage)
project_usages = described_class.for_namespace_monthly_usage(namespace_usage)
expect(project_usages).to contain_exactly(matching_project_usage)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.ciMinutesUsage' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: 'Project 1', namespace: user.namespace) }
before(:all) do
create(:ci_namespace_monthly_usage, namespace: user.namespace, amount_used: 50, date: Date.new(2021, 5, 1))
create(:ci_project_monthly_usage, project: project, amount_used: 50, date: Date.new(2021, 5, 1))
end
it 'returns usage data by month for the current user' do
query = <<-QUERY
{
ciMinutesUsage {
nodes {
minutes
month
projects {
nodes {
name
minutes
}
}
}
}
}
QUERY
post_graphql(query, current_user: user)
monthly_usage = graphql_data_at(:ci_minutes_usage, :nodes)
expect(monthly_usage).to contain_exactly({
'month' => 'May',
'minutes' => 50,
'projects' => { 'nodes' => [{
'name' => 'Project 1',
'minutes' => 50
}] }
})
end
it 'does not create N+1 queries' do
query = <<-QUERY
{
ciMinutesUsage {
nodes {
projects {
nodes {
name
}
}
}
}
}
QUERY
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user)
end
expect(graphql_errors).to be_nil
project_2 = create(:project, name: 'Project 2', namespace: user.namespace)
create(:ci_project_monthly_usage, project: project_2, amount_used: 50, date: Date.new(2021, 5, 1))
expect do
post_graphql(query, current_user: user)
end.not_to exceed_query_limit(control_count)
expect(graphql_errors).to be_nil
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