Commit 293052e2 authored by David Fernandez's avatar David Fernandez Committed by Oswaldo Ferreira

Add GraphQL update mutation for container expiration policies

Add the corresponding service
parent f96e9bf7
# frozen_string_literal: true
module Mutations
module ContainerExpirationPolicies
class Update < Mutations::BaseMutation
include ResolvesProject
graphql_name 'UpdateContainerExpirationPolicy'
authorize :destroy_container_image
argument :project_path,
GraphQL::ID_TYPE,
required: true,
description: 'The project path where the container expiration policy is located'
argument :enabled,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::ContainerExpirationPolicyType, :enabled)
argument :cadence,
Types::ContainerExpirationPolicyCadenceEnum,
required: false,
description: copy_field_description(Types::ContainerExpirationPolicyType, :cadence)
argument :older_than,
Types::ContainerExpirationPolicyOlderThanEnum,
required: false,
description: copy_field_description(Types::ContainerExpirationPolicyType, :older_than)
argument :keep_n,
Types::ContainerExpirationPolicyKeepEnum,
required: false,
description: copy_field_description(Types::ContainerExpirationPolicyType, :keep_n)
field :container_expiration_policy,
Types::ContainerExpirationPolicyType,
null: true,
description: 'The container expiration policy after mutation'
def resolve(project_path:, **args)
project = authorized_find!(full_path: project_path)
result = ::ContainerExpirationPolicies::UpdateService
.new(container: project, current_user: current_user, params: args)
.execute
{
container_expiration_policy: result.payload[:container_expiration_policy],
errors: result.error? ? [result.message] : []
}
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
......@@ -10,7 +10,7 @@ module Types
field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was created'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was updated'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates if this container expiration policy is enabled'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether this container expiration policy is enabled'
field :older_than, Types::ContainerExpirationPolicyOlderThanEnum, null: true, description: 'Tags older that this will expire'
field :cadence, Types::ContainerExpirationPolicyCadenceEnum, null: false, description: 'This container expiration policy schedule'
field :keep_n, Types::ContainerExpirationPolicyKeepEnum, null: true, description: 'Number of tags to retain'
......
......@@ -49,6 +49,7 @@ module Types
mount_mutation Mutations::JiraImport::Start
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::ContainerExpirationPolicies::Update
end
end
......
# frozen_string_literal: true
module ContainerExpirationPolicies
class UpdateService < BaseContainerService
include Gitlab::Utils::StrongMemoize
ALLOWED_ATTRIBUTES = %i[enabled cadence older_than keep_n name_regex name_regex_keep].freeze
def execute
return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
if container_expiration_policy.update(container_expiration_policy_params)
ServiceResponse.success(payload: { container_expiration_policy: container_expiration_policy })
else
ServiceResponse.error(
message: container_expiration_policy.errors.full_messages.to_sentence || 'Bad request',
http_status: 400
)
end
end
private
def container_expiration_policy
strong_memoize(:container_expiration_policy) do
@container.container_expiration_policy || @container.build_container_expiration_policy
end
end
def allowed?
Ability.allowed?(current_user, :destroy_container_image, @container)
end
def container_expiration_policy_params
@params.slice(*ALLOWED_ATTRIBUTES)
end
end
end
---
title: Add container expiration policy objects to the GraphQL API
merge_request: 32944
author:
type: added
......@@ -1084,7 +1084,7 @@ type ContainerExpirationPolicy {
createdAt: Time!
"""
Indicates if this container expiration policy is enabled
Indicates whether this container expiration policy is enabled
"""
enabled: Boolean!
......@@ -7244,6 +7244,7 @@ type Mutation {
todosMarkAllDone(input: TodosMarkAllDoneInput!): TodosMarkAllDonePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload
updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
"""
......@@ -11788,6 +11789,61 @@ type UpdateAlertStatusPayload {
issue: Issue
}
"""
Autogenerated input type of UpdateContainerExpirationPolicy
"""
input UpdateContainerExpirationPolicyInput {
"""
This container expiration policy schedule
"""
cadence: ContainerExpirationPolicyCadenceEnum
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Indicates whether this container expiration policy is enabled
"""
enabled: Boolean
"""
Number of tags to retain
"""
keepN: ContainerExpirationPolicyKeepEnum
"""
Tags older that this will expire
"""
olderThan: ContainerExpirationPolicyOlderThanEnum
"""
The project path where the container expiration policy is located
"""
projectPath: ID!
}
"""
Autogenerated return type of UpdateContainerExpirationPolicy
"""
type UpdateContainerExpirationPolicyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The container expiration policy after mutation
"""
containerExpirationPolicy: ContainerExpirationPolicy
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
input UpdateDiffImagePositionInput {
"""
Total height of the image
......
......@@ -2885,7 +2885,7 @@
},
{
"name": "enabled",
"description": "Indicates if this container expiration policy is enabled",
"description": "Indicates whether this container expiration policy is enabled",
"args": [
],
......@@ -21331,6 +21331,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateContainerExpirationPolicy",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateContainerExpirationPolicyInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateContainerExpirationPolicyPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateEpic",
"description": null,
......@@ -34861,6 +34888,148 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateContainerExpirationPolicyInput",
"description": "Autogenerated input type of UpdateContainerExpirationPolicy",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project path where the container expiration policy is located",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "enabled",
"description": "Indicates whether this container expiration policy is enabled",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "cadence",
"description": "This container expiration policy schedule",
"type": {
"kind": "ENUM",
"name": "ContainerExpirationPolicyCadenceEnum",
"ofType": null
},
"defaultValue": null
},
{
"name": "olderThan",
"description": "Tags older that this will expire",
"type": {
"kind": "ENUM",
"name": "ContainerExpirationPolicyOlderThanEnum",
"ofType": null
},
"defaultValue": null
},
{
"name": "keepN",
"description": "Number of tags to retain",
"type": {
"kind": "ENUM",
"name": "ContainerExpirationPolicyKeepEnum",
"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": "UpdateContainerExpirationPolicyPayload",
"description": "Autogenerated return type of UpdateContainerExpirationPolicy",
"fields": [
{
"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": "containerExpirationPolicy",
"description": "The container expiration policy after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ContainerExpirationPolicy",
"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": "UpdateDiffImagePositionInput",
......@@ -196,7 +196,7 @@ A tag expiration policy designed to keep only the images that matter most
| --- | ---- | ---------- |
| `cadence` | ContainerExpirationPolicyCadenceEnum! | This container expiration policy schedule |
| `createdAt` | Time! | Timestamp of when the container expiration policy was created |
| `enabled` | Boolean! | Indicates if this container expiration policy is enabled |
| `enabled` | Boolean! | Indicates whether this container expiration policy is enabled |
| `keepN` | ContainerExpirationPolicyKeepEnum | Number of tags to retain |
| `nameRegex` | String | Tags with names matching this regex pattern will expire |
| `nameRegexKeep` | String | Tags with names matching this regex pattern will be preserved |
......@@ -1755,6 +1755,16 @@ Autogenerated return type of UpdateAlertStatus
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue created after mutation |
## UpdateContainerExpirationPolicyPayload
Autogenerated return type of UpdateContainerExpirationPolicy
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## UpdateEpicPayload
Autogenerated return type of UpdateEpic
......
......@@ -297,6 +297,12 @@ FactoryBot.define do
trait :auto_devops_disabled do
association :auto_devops, factory: [:project_auto_devops, :disabled]
end
trait :without_container_expiration_policy do
after :create do |project|
project.container_expiration_policy.destroy!
end
end
end
# Project with empty repository
......
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::ContainerExpirationPolicies::Update do
using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { create(:user) }
let(:container_expiration_policy) { project.container_expiration_policy }
let(:params) { { project_path: project.full_path, cadence: '3month', keep_n: 100, older_than: '14d' } }
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
describe '#resolve' do
subject { described_class.new(object: project, context: { current_user: user }, field: nil).resolve(params) }
RSpec.shared_examples 'returning a success' do
it 'returns the container expiration policy with no errors' do
expect(subject).to eq(
container_expiration_policy: container_expiration_policy,
errors: []
)
end
end
RSpec.shared_examples 'updating the container expiration policy' do
it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
it_behaves_like 'returning a success'
context 'with invalid params' do
let_it_be(:params) { { project_path: project.full_path, cadence: '20d' } }
it_behaves_like 'not creating the container expiration policy'
it "doesn't update the cadence" do
expect { subject }
.not_to change { container_expiration_policy.reload.cadence }
end
it 'returns an error' do
expect(subject).to eq(
container_expiration_policy: nil,
errors: ['Cadence is not included in the list']
)
end
end
end
RSpec.shared_examples 'denying access to container expiration policy' do
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'with existing container expiration policy' do
where(:user_role, :shared_examples_name) do
:maintainer | 'updating the container expiration policy'
:developer | 'updating the container expiration policy'
:reporter | 'denying access to container expiration policy'
:guest | 'denying access to container expiration policy'
:anonymous | 'denying access to container expiration policy'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'without existing container expiration policy' do
let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
where(:user_role, :shared_examples_name) do
:maintainer | 'creating the container expiration policy'
:developer | 'creating the container expiration policy'
:reporter | 'denying access to container expiration policy'
:guest | 'denying access to container expiration policy'
:anonymous | 'denying access to container expiration policy'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Updating the container expiration policy' do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { create(:user) }
let(:container_expiration_policy) { project.container_expiration_policy.reload }
let(:params) do
{
project_path: project.full_path,
cadence: 'EVERY_THREE_MONTHS',
keep_n: 'ONE_HUNDRED_TAGS',
older_than: 'FOURTEEN_DAYS'
}
end
let(:mutation) do
graphql_mutation(:update_container_expiration_policy, params,
<<~QL
containerExpirationPolicy {
cadence
keepN
nameRegexKeep
nameRegex
olderThan
}
errors
QL
)
end
let(:mutation_response) { graphql_mutation_response(:update_container_expiration_policy) }
let(:container_expiration_policy_response) { mutation_response['containerExpirationPolicy'] }
RSpec.shared_examples 'returning a success' do
it_behaves_like 'returning response status', :success
it 'returns the updated container expiration policy' do
subject
expect(mutation_response['errors']).to be_empty
expect(container_expiration_policy_response['cadence']).to eq(params[:cadence])
expect(container_expiration_policy_response['keepN']).to eq(params[:keep_n])
expect(container_expiration_policy_response['olderThan']).to eq(params[:older_than])
end
end
RSpec.shared_examples 'updating the container expiration policy' do
it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
it_behaves_like 'returning a success'
end
RSpec.shared_examples 'denying access to container expiration policy' do
it_behaves_like 'not creating the container expiration policy'
it_behaves_like 'returning response status', :success
it 'returns no response' do
subject
expect(mutation_response).to be_nil
end
end
describe 'post graphql mutation' do
subject { post_graphql_mutation(mutation, current_user: user) }
context 'with existing container expiration policy' do
where(:user_role, :shared_examples_name) do
:maintainer | 'updating the container expiration policy'
:developer | 'updating the container expiration policy'
:reporter | 'denying access to container expiration policy'
:guest | 'denying access to container expiration policy'
:anonymous | 'denying access to container expiration policy'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'without existing container expiration policy' do
let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
where(:user_role, :shared_examples_name) do
:maintainer | 'creating the container expiration policy'
:developer | 'creating the container expiration policy'
:reporter | 'denying access to container expiration policy'
:guest | 'denying access to container expiration policy'
:anonymous | 'denying access to container expiration policy'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ContainerExpirationPolicies::UpdateService do
using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:params) { { cadence: '3month', keep_n: 100, older_than: '14d', extra_key: 'will_not_be_processed' } }
let(:container_expiration_policy) { project.container_expiration_policy }
describe '#execute' do
subject { described_class.new(container: project, current_user: user, params: params).execute }
RSpec.shared_examples 'returning a success' do
it 'returns a success' do
result = subject
expect(result.payload[:container_expiration_policy]).to be_present
expect(result.success?).to be_truthy
end
end
RSpec.shared_examples 'returning an error' do |message, http_status|
it 'returns an error' do
result = subject
expect(result.message).to eq(message)
expect(result.status).to eq(:error)
expect(result.http_status).to eq(http_status)
end
end
RSpec.shared_examples 'updating the container expiration policy' do
it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
it_behaves_like 'returning a success'
context 'with invalid params' do
let_it_be(:params) { { cadence: '20d' } }
it_behaves_like 'not creating the container expiration policy'
it "doesn't update the cadence" do
expect { subject }
.not_to change { container_expiration_policy.reload.cadence }
end
it_behaves_like 'returning an error', 'Cadence is not included in the list', 400
end
end
RSpec.shared_examples 'denying access to container expiration policy' do
context 'with existing container expiration policy' do
it_behaves_like 'not creating the container expiration policy'
it_behaves_like 'returning an error', 'Access Denied', 403
end
end
context 'with existing container expiration policy' do
where(:user_role, :shared_examples_name) do
:maintainer | 'updating the container expiration policy'
:developer | 'updating the container expiration policy'
:reporter | 'denying access to container expiration policy'
:guest | 'denying access to container expiration policy'
:anonymous | 'denying access to container expiration policy'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
context 'without existing container expiration policy' do
let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) }
where(:user_role, :shared_examples_name) do
:maintainer | 'creating the container expiration policy'
:developer | 'creating the container expiration policy'
:reporter | 'denying access to container expiration policy'
:guest | 'denying access to container expiration policy'
:anonymous | 'denying access to container expiration policy'
end
with_them do
before do
project.send("add_#{user_role}", user) unless user_role == :anonymous
end
it_behaves_like params[:shared_examples_name]
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'updating the container expiration policy attributes' do |mode:, from: {}, to:|
if mode == :create
it 'creates a new container expiration policy' do
expect { subject }
.to change { project.reload.container_expiration_policy.present? }.from(false).to(true)
.and change { ContainerExpirationPolicy.count }.by(1)
end
else
it_behaves_like 'not creating the container expiration policy'
end
it 'updates the container expiration policy' do
if from.empty?
subject
expect(container_expiration_policy.reload.cadence).to eq(to[:cadence])
expect(container_expiration_policy.keep_n).to eq(to[:keep_n])
expect(container_expiration_policy.older_than).to eq(to[:older_than])
else
expect { subject }
.to change { container_expiration_policy.reload.cadence }.from(from[:cadence]).to(to[:cadence])
.and change { container_expiration_policy.reload.keep_n }.from(from[:keep_n]).to(to[:keep_n])
.and change { container_expiration_policy.reload.older_than }.from(from[:older_than]).to(to[:older_than])
end
end
end
RSpec.shared_examples 'not creating the container expiration policy' do
it "doesn't create the container expiration policy" do
expect { subject }.not_to change { ContainerExpirationPolicy.count }
end
end
RSpec.shared_examples 'creating the container expiration policy' do
it_behaves_like 'updating the container expiration policy attributes', mode: :create, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
it_behaves_like 'returning a success'
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