Commit 76a1f5b2 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '216571-terraform-state-mutations' into 'master'

Add GraphQL endpoints to lock, unlock and delete Terraform states

See merge request gitlab-org/gitlab!43955
parents e429e473 188aad96
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Base < BaseMutation
authorize :admin_terraform_state
argument :id,
Types::GlobalIDType[::Terraform::State],
required: true,
description: 'Global ID of the Terraform state'
private
def find_object(id:)
GitlabSchema.find_by_gid(id)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Delete < Base
graphql_name 'TerraformStateDelete'
def resolve(id:)
state = authorized_find!(id: id)
state.destroy
{ errors: errors_on_object(state) }
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Lock < Base
graphql_name 'TerraformStateLock'
def resolve(id:)
state = authorized_find!(id: id)
if state.locked?
state.errors.add(:base, 'state is already locked')
else
state.update(lock_xid: lock_xid, locked_by_user: current_user, locked_at: Time.current)
end
{ errors: errors_on_object(state) }
end
private
def lock_xid
SecureRandom.uuid
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Unlock < Base
graphql_name 'TerraformStateUnlock'
def resolve(id:)
state = authorized_find!(id: id)
state.update(lock_xid: nil, locked_by_user: nil, locked_at: nil)
{ errors: errors_on_object(state) }
end
end
end
end
end
......@@ -54,6 +54,9 @@ module Types
'If the body of the Note contains only quick actions, the Note will be ' \
'destroyed during the update, and no Note will be returned'
mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
mount_mutation Mutations::Todos::MarkAllDone
......
---
title: Add GraphQL endpoints to lock, unlock and delete Terraform states
merge_request: 43955
author:
type: added
......@@ -12531,6 +12531,9 @@ type Mutation {
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload @deprecated(reason: "Use vulnerabilityRevertToDetected. Deprecated in 13.5")
runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @deprecated(reason: "Use DastOnDemandScanCreate. Deprecated in 13.4")
terraformStateDelete(input: TerraformStateDeleteInput!): TerraformStateDeletePayload
terraformStateLock(input: TerraformStateLockInput!): TerraformStateLockPayload
terraformStateUnlock(input: TerraformStateUnlockInput!): TerraformStateUnlockPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
......@@ -19010,6 +19013,36 @@ type TerraformStateConnection {
pageInfo: PageInfo!
}
"""
Autogenerated input type of TerraformStateDelete
"""
input TerraformStateDeleteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the Terraform state
"""
id: TerraformStateID!
}
"""
Autogenerated return type of TerraformStateDelete
"""
type TerraformStateDeletePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
An edge in a connection.
"""
......@@ -19025,6 +19058,71 @@ type TerraformStateEdge {
node: TerraformState
}
"""
Identifier of Terraform::State
"""
scalar TerraformStateID
"""
Autogenerated input type of TerraformStateLock
"""
input TerraformStateLockInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the Terraform state
"""
id: TerraformStateID!
}
"""
Autogenerated return type of TerraformStateLock
"""
type TerraformStateLockPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Autogenerated input type of TerraformStateUnlock
"""
input TerraformStateUnlockInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the Terraform state
"""
id: TerraformStateID!
}
"""
Autogenerated return type of TerraformStateUnlock
"""
type TerraformStateUnlockPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Represents the Geo sync and verification state of a terraform state version
"""
......
......@@ -2629,6 +2629,33 @@ Completion status of tasks.
| `name` | String! | Name of the Terraform state |
| `updatedAt` | Time! | Timestamp the Terraform state was updated |
### TerraformStateDeletePayload
Autogenerated return type of TerraformStateDelete.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### TerraformStateLockPayload
Autogenerated return type of TerraformStateLock.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### TerraformStateUnlockPayload
Autogenerated return type of TerraformStateUnlock.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### TerraformStateVersionRegistry
Represents the Geo sync and verification state of a terraform state version.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Terraform::State::Delete do
let_it_be(:user) { create(:user) }
let_it_be(:state) { create(:terraform_state) }
let(:mutation) do
described_class.new(
object: double,
context: { current_user: user },
field: double
)
end
it { expect(described_class.graphql_name).to eq('TerraformStateDelete') }
it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
describe '#resolve' do
let(:global_id) { state.to_global_id }
subject { mutation.resolve(id: global_id) }
context 'user does not have permission' do
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { state.reload }.not_to raise_error
end
end
context 'user has permission' do
before do
state.project.add_maintainer(user)
end
it 'deletes the state', :aggregate_failures do
expect do
expect(subject).to eq(errors: [])
end.to change { ::Terraform::State.count }.by(-1)
expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'with invalid params' do
let(:global_id) { user.to_global_id }
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { state.reload }.not_to raise_error
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Terraform::State::Lock do
let_it_be(:user) { create(:user) }
let_it_be(:state) { create(:terraform_state) }
let(:mutation) do
described_class.new(
object: double,
context: { current_user: user },
field: double
)
end
it { expect(described_class.graphql_name).to eq('TerraformStateLock') }
it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
describe '#resolve' do
let(:global_id) { state.to_global_id }
subject { mutation.resolve(id: global_id) }
context 'user does not have permission' do
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).not_to be_locked
end
end
context 'user has permission' do
before do
state.project.add_maintainer(user)
end
it 'locks the state', :aggregate_failures do
expect(subject).to eq(errors: [])
expect(state.reload).to be_locked
expect(state.locked_by_user).to eq(user)
expect(state.lock_xid).to be_present
expect(state.locked_at).to be_present
end
context 'state is already locked' do
let(:locked_by_user) { create(:user) }
let(:state) { create(:terraform_state, :locked, locked_by_user: locked_by_user) }
it 'does not modify the existing lock', :aggregate_failures do
expect(subject).to eq(errors: ['state is already locked'])
expect(state.reload).to be_locked
expect(state.locked_by_user).to eq(locked_by_user)
end
end
end
context 'with invalid params' do
let(:global_id) { user.to_global_id }
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).not_to be_locked
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Terraform::State::Unlock do
let_it_be(:user) { create(:user) }
let_it_be(:state) { create(:terraform_state, :locked) }
let(:mutation) do
described_class.new(
object: double,
context: { current_user: user },
field: double
)
end
it { expect(described_class.graphql_name).to eq('TerraformStateUnlock') }
it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
describe '#resolve' do
let(:global_id) { state.to_global_id }
subject { mutation.resolve(id: global_id) }
context 'user does not have permission' do
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).to be_locked
end
end
context 'user has permission' do
before do
state.project.add_maintainer(user)
end
it 'unlocks the state', :aggregate_failures do
expect(subject).to eq(errors: [])
expect(state.reload).not_to be_locked
end
context 'state is already unlocked' do
let(:state) { create(:terraform_state) }
it 'does not modify the state' do
expect(subject).to eq(errors: [])
expect(state.reload).not_to be_locked
end
end
end
context 'with invalid params' do
let(:global_id) { user.to_global_id }
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).to be_locked
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'delete a terraform state' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let(:state) { create(:terraform_state, project: project) }
let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) }
before do
post_graphql_mutation(mutation, current_user: user)
end
include_examples 'a working graphql query'
it 'deletes the state' do
expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'lock a terraform state' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let(:state) { create(:terraform_state, project: project) }
let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) }
before do
expect(state).not_to be_locked
post_graphql_mutation(mutation, current_user: user)
end
include_examples 'a working graphql query'
it 'locks the state' do
expect(state.reload).to be_locked
expect(state.locked_by_user).to eq(user)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'unlock a terraform state' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let(:state) { create(:terraform_state, :locked, project: project) }
let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) }
before do
expect(state).to be_locked
post_graphql_mutation(mutation, current_user: user)
end
include_examples 'a working graphql query'
it 'unlocks the state' do
expect(state.reload).not_to be_locked
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