2020-04-22 19:07:51 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require_dependency 'api/validations/validators/limit'
|
|
|
|
|
|
|
|
module API
|
|
|
|
module Terraform
|
2021-01-03 14:25:43 +05:30
|
|
|
class State < ::API::Base
|
2020-04-22 19:07:51 +05:30
|
|
|
include ::Gitlab::Utils::StrongMemoize
|
|
|
|
|
2021-01-29 00:20:46 +05:30
|
|
|
feature_category :infrastructure_as_code
|
2022-07-16 23:28:13 +05:30
|
|
|
urgency :low
|
2021-01-29 00:20:46 +05:30
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
default_format :json
|
|
|
|
|
2022-01-26 12:08:38 +05:30
|
|
|
rescue_from(
|
2022-07-23 23:45:48 +05:30
|
|
|
::Terraform::RemoteStateHandler::StateDeletedError,
|
2022-01-26 12:08:38 +05:30
|
|
|
::ActiveRecord::RecordNotUnique,
|
|
|
|
::PG::UniqueViolation
|
|
|
|
) do |e|
|
|
|
|
render_api_error!(e.message, 422)
|
|
|
|
end
|
|
|
|
|
2023-03-04 22:38:38 +05:30
|
|
|
STATE_NAME_URI_REQUIREMENTS = { name: API::NO_SLASH_URL_PART_REGEX }.freeze
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
before do
|
|
|
|
authenticate!
|
2020-07-28 23:09:34 +05:30
|
|
|
authorize! :read_terraform_state, user_project
|
2021-03-08 18:12:59 +05:30
|
|
|
|
|
|
|
increment_unique_values('p_terraform_state_api_unique_users', current_user.id)
|
2022-07-23 23:45:48 +05:30
|
|
|
|
2023-05-27 22:25:52 +05:30
|
|
|
Gitlab::Tracking.event(
|
|
|
|
'API::Terraform::State',
|
|
|
|
'terraform_state_api_request',
|
|
|
|
namespace: user_project&.namespace,
|
|
|
|
user: current_user,
|
|
|
|
project: user_project,
|
|
|
|
label: 'redis_hll_counters.terraform.p_terraform_state_api_unique_users_monthly',
|
|
|
|
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
|
|
|
|
event: 'p_terraform_state_api_unique_users').to_context]
|
|
|
|
)
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
params do
|
2023-01-13 00:05:48 +05:30
|
|
|
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
2023-03-04 22:38:38 +05:30
|
|
|
namespace ':id/terraform/state/:name', requirements: STATE_NAME_URI_REQUIREMENTS do
|
2020-04-22 19:07:51 +05:30
|
|
|
params do
|
|
|
|
requires :name, type: String, desc: 'The name of a Terraform state'
|
|
|
|
optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
|
|
|
|
end
|
|
|
|
|
|
|
|
helpers do
|
|
|
|
def remote_state_handler
|
|
|
|
::Terraform::RemoteStateHandler.new(user_project, current_user, name: params[:name], lock_id: params[:ID])
|
|
|
|
end
|
2023-03-04 22:38:38 +05:30
|
|
|
|
|
|
|
def not_found_for_dots?
|
|
|
|
Feature.disabled?(:allow_dots_on_tf_state_names) && params[:name].include?(".")
|
|
|
|
end
|
|
|
|
|
|
|
|
# Change the state name to behave like before, https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105674
|
|
|
|
# has been introduced. This behavior can be controlled via `allow_dots_on_tf_state_names` FF.
|
|
|
|
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861
|
|
|
|
def legacy_state_name!
|
|
|
|
params[:name] = params[:name].split('.').first
|
|
|
|
end
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
desc 'Get a Terraform state by its name' do
|
|
|
|
detail 'Get a Terraform state by its name'
|
|
|
|
success [
|
|
|
|
{ code: 200 },
|
|
|
|
{ code: 204, message: 'Empty state' }
|
|
|
|
]
|
|
|
|
failure [
|
|
|
|
{ code: 403, message: 'Forbidden' },
|
|
|
|
{ code: 404, message: 'Not found' },
|
|
|
|
{ code: 422, message: 'Validation failure' }
|
|
|
|
]
|
|
|
|
tags %w[terraform_state]
|
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
2020-04-22 19:07:51 +05:30
|
|
|
get do
|
2023-03-04 22:38:38 +05:30
|
|
|
legacy_state_name! if not_found_for_dots?
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
remote_state_handler.find_with_lock do |state|
|
2020-11-24 15:15:51 +05:30
|
|
|
no_content! unless state.latest_file && state.latest_file.exists?
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
env['api.format'] = :binary # this bypasses json serialization
|
2020-11-24 15:15:51 +05:30
|
|
|
body state.latest_file.read
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
desc 'Add a new Terraform state or update an existing one' do
|
|
|
|
detail 'Add a new Terraform state or update an existing one'
|
|
|
|
success [
|
|
|
|
{ code: 200 },
|
|
|
|
{ code: 204, message: 'No data provided' }
|
|
|
|
]
|
|
|
|
failure [
|
|
|
|
{ code: 403, message: 'Forbidden' },
|
2023-03-04 22:38:38 +05:30
|
|
|
{ code: 422, message: 'Validation failure' },
|
|
|
|
{ code: 413, message: 'Request Entity Too Large' }
|
2023-01-13 00:05:48 +05:30
|
|
|
]
|
|
|
|
tags %w[terraform_state]
|
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
2020-04-22 19:07:51 +05:30
|
|
|
post do
|
2020-07-28 23:09:34 +05:30
|
|
|
authorize! :admin_terraform_state, user_project
|
2023-03-04 22:38:38 +05:30
|
|
|
legacy_state_name! if not_found_for_dots?
|
2020-07-28 23:09:34 +05:30
|
|
|
|
2020-05-05 14:28:15 +05:30
|
|
|
data = request.body.read
|
2020-04-22 19:07:51 +05:30
|
|
|
no_content! if data.empty?
|
|
|
|
|
2023-03-04 22:38:38 +05:30
|
|
|
max_state_size = Gitlab::CurrentSettings.max_terraform_state_size_bytes
|
|
|
|
file_too_large! if max_state_size > 0 && data.size > max_state_size
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
remote_state_handler.handle_with_lock do |state|
|
2021-01-29 00:20:46 +05:30
|
|
|
state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial], build: current_authenticated_job)
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
2020-11-05 12:06:23 +05:30
|
|
|
|
|
|
|
body false
|
|
|
|
status :ok
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
desc 'Delete a Terraform state of a certain name' do
|
|
|
|
detail 'Delete a Terraform state of a certain name'
|
|
|
|
success code: 200
|
|
|
|
failure [
|
|
|
|
{ code: 403, message: 'Forbidden' },
|
|
|
|
{ code: 404, message: 'Not found' },
|
|
|
|
{ code: 422, message: 'Validation failure' }
|
|
|
|
]
|
|
|
|
tags %w[terraform_state]
|
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
2020-04-22 19:07:51 +05:30
|
|
|
delete do
|
2020-07-28 23:09:34 +05:30
|
|
|
authorize! :admin_terraform_state, user_project
|
2023-03-04 22:38:38 +05:30
|
|
|
legacy_state_name! if not_found_for_dots?
|
2020-07-28 23:09:34 +05:30
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
remote_state_handler.find_with_lock do |state|
|
2022-07-23 23:45:48 +05:30
|
|
|
::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
2020-11-05 12:06:23 +05:30
|
|
|
|
|
|
|
body false
|
|
|
|
status :ok
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
desc 'Lock a Terraform state of a certain name' do
|
|
|
|
detail 'Lock a Terraform state of a certain name'
|
|
|
|
success code: 200
|
|
|
|
failure [
|
|
|
|
{ code: 403, message: 'Forbidden' },
|
|
|
|
{ code: 404, message: 'Not found' },
|
|
|
|
{ code: 409, message: 'Conflict' },
|
|
|
|
{ code: 422, message: 'Validation failure' }
|
|
|
|
]
|
|
|
|
tags %w[terraform_state]
|
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
2020-04-22 19:07:51 +05:30
|
|
|
params do
|
|
|
|
requires :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
|
|
|
|
requires :Operation, type: String, desc: 'Terraform operation'
|
|
|
|
requires :Info, type: String, desc: 'Terraform info'
|
|
|
|
requires :Who, type: String, desc: 'Terraform state lock owner'
|
|
|
|
requires :Version, type: String, desc: 'Terraform version'
|
|
|
|
requires :Created, type: String, desc: 'Terraform state lock timestamp'
|
|
|
|
requires :Path, type: String, desc: 'Terraform path'
|
|
|
|
end
|
|
|
|
post '/lock' do
|
2023-03-04 22:38:38 +05:30
|
|
|
not_found! if not_found_for_dots?
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
authorize! :admin_terraform_state, user_project
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
status_code = :ok
|
|
|
|
lock_info = {
|
|
|
|
'Operation' => params[:Operation],
|
|
|
|
'Info' => params[:Info],
|
|
|
|
'Version' => params[:Version],
|
|
|
|
'Path' => params[:Path]
|
|
|
|
}
|
|
|
|
|
|
|
|
begin
|
|
|
|
remote_state_handler.lock!
|
|
|
|
rescue ::Terraform::RemoteStateHandler::StateLockedError
|
|
|
|
status_code = :conflict
|
|
|
|
end
|
|
|
|
|
|
|
|
remote_state_handler.find_with_lock do |state|
|
|
|
|
lock_info['ID'] = state.lock_xid
|
|
|
|
lock_info['Who'] = state.locked_by_user.username
|
|
|
|
lock_info['Created'] = state.locked_at
|
|
|
|
|
|
|
|
env['api.format'] = :binary # this bypasses json serialization
|
|
|
|
body lock_info.to_json
|
|
|
|
status status_code
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-13 00:05:48 +05:30
|
|
|
desc 'Unlock a Terraform state of a certain name' do
|
|
|
|
detail 'Unlock a Terraform state of a certain name'
|
|
|
|
success code: 200
|
|
|
|
failure [
|
|
|
|
{ code: 403, message: 'Forbidden' },
|
|
|
|
{ code: 404, message: 'Not found' },
|
|
|
|
{ code: 409, message: 'Conflict' },
|
|
|
|
{ code: 422, message: 'Validation failure' }
|
|
|
|
]
|
|
|
|
tags %w[terraform_state]
|
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
2020-04-22 19:07:51 +05:30
|
|
|
params do
|
|
|
|
optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
|
|
|
|
end
|
|
|
|
delete '/lock' do
|
2023-03-04 22:38:38 +05:30
|
|
|
not_found! if not_found_for_dots?
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
authorize! :admin_terraform_state, user_project
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
remote_state_handler.unlock!
|
|
|
|
status :ok
|
|
|
|
rescue ::Terraform::RemoteStateHandler::StateLockedError
|
|
|
|
status :conflict
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|