Commit e9a27e4a authored by Michael Kozono's avatar Michael Kozono

Merge branch '291140-query-single-state' into 'master'

Add ability to query single Terraform State

See merge request gitlab-org/gitlab!51145
parents 2beec806 58572385
# frozen_string_literal: true
module Terraform
class StatesFinder
def initialize(project, current_user, params: {})
@project = project
@current_user = current_user
@params = params
end
def execute
return ::Terraform::State.none unless can_read_terraform_states?
states = project.terraform_states
states = states.with_name(params[:name]) if params[:name].present?
states.ordered_by_name
end
private
attr_reader :project, :current_user, :params
def can_read_terraform_states?
current_user.can?(:read_terraform_state, project)
end
end
end
......@@ -3,20 +3,20 @@
module Resolvers
module Terraform
class StatesResolver < BaseResolver
type Types::Terraform::StateType, null: true
type Types::Terraform::StateType.connection_type, null: true
alias_method :project, :object
def resolve(**args)
return ::Terraform::State.none unless can_read_terraform_states?
project.terraform_states.ordered_by_name
when_single do
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the Terraform state.'
end
private
def can_read_terraform_states?
current_user.can?(:read_terraform_state, project)
def resolve(**args)
::Terraform::StatesFinder
.new(project, current_user, params: args)
.execute
end
end
end
......
......@@ -309,10 +309,16 @@ module Types
description: 'Title of the label'
end
field :terraform_state,
Types::Terraform::StateType,
null: true,
description: 'Find a single Terraform state by name.',
resolver: Resolvers::Terraform::StatesResolver.single
field :terraform_states,
Types::Terraform::StateType.connection_type,
null: true,
description: 'Terraform states associated with the project',
description: 'Terraform states associated with the project.',
resolver: Resolvers::Terraform::StatesResolver
field :pipeline_analytics, Types::Ci::AnalyticsType, null: true,
......
......@@ -22,6 +22,7 @@ module Terraform
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
validates :project_id, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
......
---
title: Add GraphQL query for single Terraform state
merge_request: 51145
author:
type: added
......@@ -19316,7 +19316,17 @@ type Project {
tagList: String
"""
Terraform states associated with the project
Find a single Terraform state by name.
"""
terraformState(
"""
Name of the Terraform state.
"""
name: String!
): TerraformState
"""
Terraform states associated with the project.
"""
terraformStates(
"""
......
......@@ -56122,9 +56122,36 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "terraformState",
"description": "Find a single Terraform state by name.",
"args": [
{
"name": "name",
"description": "Name of the Terraform state.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TerraformState",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "terraformStates",
"description": "Terraform states associated with the project",
"description": "Terraform states associated with the project.",
"args": [
{
"name": "after",
......@@ -2787,7 +2787,8 @@ Autogenerated return type of PipelineRetry.
| `statistics` | ProjectStatistics | Statistics of the project |
| `suggestionCommitMessage` | String | The commit message used to apply merge request suggestions |
| `tagList` | String | List of project topics (not Git tags) |
| `terraformStates` | TerraformStateConnection | Terraform states associated with the project |
| `terraformState` | TerraformState | Find a single Terraform state by name. |
| `terraformStates` | TerraformStateConnection | Terraform states associated with the project. |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the project |
| `vulnerabilities` | VulnerabilityConnection | Vulnerabilities reported on the project |
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Terraform::StatesFinder do
describe '#execute' do
let_it_be(:project) { create(:project) }
let_it_be(:state_1) { create(:terraform_state, project: project) }
let_it_be(:state_2) { create(:terraform_state, project: project) }
let(:user) { project.creator }
subject { described_class.new(project, user).execute }
it { is_expected.to contain_exactly(state_1, state_2) }
context 'user does not have permission' do
let(:user) { create(:user) }
before do
project.add_guest(user)
end
it { is_expected.to be_empty }
end
context 'filtering by name' do
let(:params) { { name: name_param } }
subject { described_class.new(project, user, params: params).execute }
context 'name does not match' do
let(:name_param) { 'other-name' }
it { is_expected.to be_empty }
end
context 'name does match' do
let(:name_param) { state_1.name }
it { is_expected.to contain_exactly(state_1) }
end
end
end
end
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Resolvers::Terraform::StatesResolver do
include GraphqlHelpers
it { expect(described_class.type).to eq(Types::Terraform::StateType) }
it { expect(described_class).to have_nullable_graphql_type(Types::Terraform::StateType.connection_type) }
it { expect(described_class.null).to be_truthy }
describe '#resolve' do
......@@ -31,3 +31,21 @@ RSpec.describe Resolvers::Terraform::StatesResolver do
end
end
end
RSpec.describe Resolvers::Terraform::StatesResolver.single do
it { expect(described_class).to be < Resolvers::Terraform::StatesResolver }
describe 'arguments' do
subject { described_class.arguments[argument] }
describe 'name' do
let(:argument) { 'name' }
it do
expect(subject).to be_present
expect(subject.type.to_s).to eq('String!')
expect(subject.description).to be_present
end
end
end
end
......@@ -318,6 +318,13 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) }
end
describe 'terraform state field' do
subject { described_class.fields['terraformState'] }
it { is_expected.to have_graphql_type(Types::Terraform::StateType) }
it { is_expected.to have_graphql_resolver(Resolvers::Terraform::StatesResolver.single) }
end
describe 'terraform states field' do
subject { described_class.fields['terraformStates'] }
......
......@@ -25,6 +25,15 @@ RSpec.describe Terraform::State do
it { expect(subject.map(&:name)).to eq(names.sort) }
end
describe '.with_name' do
let_it_be(:matching_name) { create(:terraform_state, name: 'matching-name') }
let_it_be(:other_name) { create(:terraform_state, name: 'other-name') }
subject { described_class.with_name(matching_name.name) }
it { is_expected.to contain_exactly(matching_name) }
end
end
describe '#destroy' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'query a single terraform state' do
include GraphqlHelpers
include ::API::Helpers::RelatedResourcesHelpers
let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked) }
let(:latest_version) { terraform_state.latest_version }
let(:project) { terraform_state.project }
let(:current_user) { project.creator }
let(:data) { graphql_data.dig('project', 'terraformState') }
let(:query) do
graphql_query_for(
:project,
{ fullPath: project.full_path },
query_graphql_field(
:terraformState,
{ name: terraform_state.name },
%{
id
name
lockedAt
createdAt
updatedAt
latestVersion {
id
serial
createdAt
updatedAt
createdByUser {
id
}
job {
name
}
}
lockedByUser {
id
}
}
)
)
end
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'returns terraform state data' do
expect(data).to match(a_hash_including({
'id' => global_id_of(terraform_state),
'name' => terraform_state.name,
'lockedAt' => terraform_state.locked_at.iso8601,
'createdAt' => terraform_state.created_at.iso8601,
'updatedAt' => terraform_state.updated_at.iso8601,
'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) },
'latestVersion' => {
'id' => eq(global_id_of(latest_version)),
'serial' => eq(latest_version.version),
'createdAt' => eq(latest_version.created_at.iso8601),
'updatedAt' => eq(latest_version.updated_at.iso8601),
'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) },
'job' => { 'name' => eq(latest_version.build.name) }
}
}))
end
context 'unauthorized users' do
let(:current_user) { nil }
it { expect(data).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