2020-04-22 19:07:51 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
RSpec.describe API::Terraform::State, :snowplow do
|
2020-06-23 00:09:42 +05:30
|
|
|
include HttpBasicAuthHelpers
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
let_it_be(:project) { create(:project) }
|
|
|
|
let_it_be(:developer) { create(:user, developer_projects: [project]) }
|
|
|
|
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
let!(:state) { create(:terraform_state, :with_version, project: project) }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
let(:current_user) { maintainer }
|
2020-06-23 00:09:42 +05:30
|
|
|
let(:auth_header) { user_basic_auth_header(current_user) }
|
2020-04-22 19:07:51 +05:30
|
|
|
let(:project_id) { project.id }
|
|
|
|
let(:state_name) { state.name }
|
|
|
|
let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}" }
|
|
|
|
|
|
|
|
before do
|
2021-01-03 14:25:43 +05:30
|
|
|
stub_terraform_state_object_storage
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
shared_examples 'endpoint with unique user tracking' do
|
|
|
|
context 'without authentication' do
|
|
|
|
let(:auth_header) { basic_auth_header('bad', 'token') }
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
it 'does not track unique hll event' do
|
2021-03-08 18:12:59 +05:30
|
|
|
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
|
|
|
|
|
|
|
|
request
|
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
|
|
|
|
it 'does not track Snowplow event' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect_no_snowplow_event
|
|
|
|
end
|
2021-03-08 18:12:59 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'with maintainer permissions' do
|
|
|
|
let(:current_user) { maintainer }
|
|
|
|
|
2021-03-11 19:13:27 +05:30
|
|
|
it_behaves_like 'tracking unique hll events' do
|
2022-05-07 20:08:51 +05:30
|
|
|
let(:target_event) { 'p_terraform_state_api_unique_users' }
|
|
|
|
let(:expected_value) { instance_of(Integer) }
|
2021-03-08 18:12:59 +05:30
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
|
|
|
|
it 'tracks Snowplow event' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect_snowplow_event(
|
|
|
|
category: described_class.to_s,
|
|
|
|
action: 'p_terraform_state_api_unique_users',
|
|
|
|
namespace: project.namespace.reload,
|
|
|
|
user: current_user
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when route_hll_to_snowplow_phase2 FF is disabled' do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(route_hll_to_snowplow_phase2: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not track Snowplow event' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect_no_snowplow_event
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_context 'cannot access a state that is scheduled for deletion' do
|
|
|
|
before do
|
|
|
|
state.update!(deleted_at: Time.current)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns unprocessable entity' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
2021-03-08 18:12:59 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
describe 'GET /projects/:id/terraform/state/:name' do
|
|
|
|
subject(:request) { get api(state_path), headers: auth_header }
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
it_behaves_like 'endpoint with unique user tracking'
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
context 'without authentication' do
|
2020-06-23 00:09:42 +05:30
|
|
|
let(:auth_header) { basic_auth_header('bad', 'token') }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
it 'returns 401 if user is not authenticated' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
context 'personal acceess token authentication' do
|
|
|
|
context 'with maintainer permissions' do
|
|
|
|
let(:current_user) { maintainer }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
it 'returns terraform state belonging to a project of given state name' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-24 15:15:51 +05:30
|
|
|
expect(response.body).to eq(state.reload.latest_file.read)
|
2020-06-23 00:09:42 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'for a project that does not exist' do
|
|
|
|
let(:project_id) { '0000' }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
it 'returns not found' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
|
|
|
end
|
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
|
|
|
|
it_behaves_like 'cannot access a state that is scheduled for deletion'
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
context 'with developer permissions' do
|
|
|
|
let(:current_user) { developer }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
it 'returns terraform state belonging to a project of given state name' do
|
2020-04-22 19:07:51 +05:30
|
|
|
request
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-24 15:15:51 +05:30
|
|
|
expect(response.body).to eq(state.reload.latest_file.read)
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
context 'job token authentication' do
|
|
|
|
let(:auth_header) { job_basic_auth_header(job) }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
context 'with maintainer permissions' do
|
2020-09-03 11:15:55 +05:30
|
|
|
let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
it 'returns terraform state belonging to a project of given state name' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-24 15:15:51 +05:30
|
|
|
expect(response.body).to eq(state.reload.latest_file.read)
|
2020-06-23 00:09:42 +05:30
|
|
|
end
|
|
|
|
|
2020-09-03 11:15:55 +05:30
|
|
|
it 'returns unauthorized if the the job is not running' do
|
|
|
|
job.update!(status: :failed)
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
|
|
end
|
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
context 'for a project that does not exist' do
|
|
|
|
let(:project_id) { '0000' }
|
|
|
|
|
|
|
|
it 'returns not found' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with developer permissions' do
|
2020-09-03 11:15:55 +05:30
|
|
|
let(:job) { create(:ci_build, status: :running, project: project, user: developer) }
|
2020-06-23 00:09:42 +05:30
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
it 'returns terraform state belonging to a project of given state name' do
|
2020-06-23 00:09:42 +05:30
|
|
|
request
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-24 15:15:51 +05:30
|
|
|
expect(response.body).to eq(state.reload.latest_file.read)
|
2020-06-23 00:09:42 +05:30
|
|
|
end
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'POST /projects/:id/terraform/state/:name' do
|
2021-01-29 00:20:46 +05:30
|
|
|
let(:params) { { 'instance': 'example-instance', 'serial': state.latest_version.version + 1 } }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params }
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
it_behaves_like 'endpoint with unique user tracking'
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
context 'when terraform state with a given name is already present' do
|
|
|
|
context 'with maintainer permissions' do
|
|
|
|
let(:current_user) { maintainer }
|
|
|
|
|
|
|
|
it 'updates the state' do
|
|
|
|
expect { request }.to change { Terraform::State.count }.by(0)
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-05 12:06:23 +05:30
|
|
|
expect(Gitlab::Json.parse(response.body)).to be_empty
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
2022-01-26 12:08:38 +05:30
|
|
|
|
|
|
|
context 'when serial already exists' do
|
|
|
|
let(:params) { { 'instance': 'example-instance', 'serial': state.latest_version.version } }
|
|
|
|
|
|
|
|
it 'returns unprocessable entity' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
|
|
|
end
|
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
|
|
|
|
it_behaves_like 'cannot access a state that is scheduled for deletion'
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'without body' do
|
|
|
|
let(:params) { nil }
|
|
|
|
|
|
|
|
it 'returns no content if no body is provided' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:no_content)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with developer permissions' do
|
|
|
|
let(:current_user) { developer }
|
|
|
|
|
|
|
|
it 'returns forbidden' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when there is no terraform state of a given name' do
|
|
|
|
let(:state_name) { 'example2' }
|
|
|
|
|
|
|
|
context 'with maintainer permissions' do
|
|
|
|
let(:current_user) { maintainer }
|
|
|
|
|
|
|
|
it 'creates a new state' do
|
|
|
|
expect { request }.to change { Terraform::State.count }.by(1)
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-05 12:06:23 +05:30
|
|
|
expect(Gitlab::Json.parse(response.body)).to be_empty
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'without body' do
|
|
|
|
let(:params) { nil }
|
|
|
|
|
|
|
|
it 'returns no content if no body is provided' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:no_content)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with developer permissions' do
|
|
|
|
let(:current_user) { developer }
|
|
|
|
|
|
|
|
it 'returns forbidden' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2021-01-29 00:20:46 +05:30
|
|
|
|
|
|
|
context 'when using job token authentication' do
|
|
|
|
let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) }
|
|
|
|
let(:auth_header) { job_basic_auth_header(job) }
|
|
|
|
|
|
|
|
it 'associates the job with the newly created state version' do
|
|
|
|
expect { request }.to change { state.versions.count }.by(1)
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(state.reload_latest_version.build).to eq(job)
|
|
|
|
end
|
|
|
|
end
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
describe 'DELETE /projects/:id/terraform/state/:name' do
|
|
|
|
subject(:request) { delete api(state_path), headers: auth_header }
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
it_behaves_like 'endpoint with unique user tracking'
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
context 'with maintainer permissions' do
|
|
|
|
let(:current_user) { maintainer }
|
2022-07-23 23:45:48 +05:30
|
|
|
let(:deletion_service) { instance_double(Terraform::States::TriggerDestroyService) }
|
|
|
|
|
|
|
|
it 'schedules the state for deletion and returns empty body' do
|
|
|
|
expect(Terraform::States::TriggerDestroyService).to receive(:new).and_return(deletion_service)
|
|
|
|
expect(deletion_service).to receive(:execute).once
|
2020-04-22 19:07:51 +05:30
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
request
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2020-11-05 12:06:23 +05:30
|
|
|
expect(Gitlab::Json.parse(response.body)).to be_empty
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
|
|
|
|
it_behaves_like 'cannot access a state that is scheduled for deletion'
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'with developer permissions' do
|
|
|
|
let(:current_user) { developer }
|
|
|
|
|
|
|
|
it 'returns forbidden' do
|
|
|
|
expect { request }.to change { Terraform::State.count }.by(0)
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'PUT /projects/:id/terraform/state/:name/lock' do
|
|
|
|
let(:params) do
|
|
|
|
{
|
|
|
|
ID: '123-456',
|
|
|
|
Version: '0.1',
|
|
|
|
Operation: 'OperationTypePlan',
|
|
|
|
Info: '',
|
|
|
|
Who: "#{current_user.username}",
|
|
|
|
Created: Time.now.utc.iso8601(6),
|
|
|
|
Path: ''
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
subject(:request) { post api("#{state_path}/lock"), headers: auth_header, params: params }
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
it_behaves_like 'endpoint with unique user tracking'
|
2022-07-23 23:45:48 +05:30
|
|
|
it_behaves_like 'cannot access a state that is scheduled for deletion'
|
2021-03-08 18:12:59 +05:30
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
it 'locks the terraform state' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
2020-07-28 23:09:34 +05:30
|
|
|
|
|
|
|
context 'state is already locked' do
|
|
|
|
before do
|
|
|
|
state.update!(lock_xid: 'locked', locked_by_user: current_user)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an error' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:conflict)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'user does not have permission to lock the state' do
|
|
|
|
let(:current_user) { developer }
|
|
|
|
|
|
|
|
it 'returns an error' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
|
|
end
|
|
|
|
end
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
describe 'DELETE /projects/:id/terraform/state/:name/lock' do
|
2020-07-28 23:09:34 +05:30
|
|
|
let(:params) do
|
|
|
|
{
|
|
|
|
ID: lock_id,
|
|
|
|
Version: '0.1',
|
|
|
|
Operation: 'OperationTypePlan',
|
|
|
|
Info: '',
|
|
|
|
Who: "#{current_user.username}",
|
|
|
|
Created: Time.now.utc.iso8601(6),
|
|
|
|
Path: ''
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
before do
|
|
|
|
state.lock_xid = '123-456'
|
|
|
|
state.save!
|
|
|
|
end
|
|
|
|
|
|
|
|
subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params }
|
|
|
|
|
2021-03-08 18:12:59 +05:30
|
|
|
it_behaves_like 'endpoint with unique user tracking' do
|
|
|
|
let(:lock_id) { 'irrelevant to this test, just needs to be present' }
|
|
|
|
end
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
it_behaves_like 'cannot access a state that is scheduled for deletion' do
|
|
|
|
let(:lock_id) { 'irrelevant to this test, just needs to be present' }
|
|
|
|
end
|
|
|
|
|
2020-04-22 19:07:51 +05:30
|
|
|
context 'with the correct lock id' do
|
2020-07-28 23:09:34 +05:30
|
|
|
let(:lock_id) { '123-456' }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
it 'removes the terraform state lock' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with no lock id (force-unlock)' do
|
|
|
|
let(:params) { {} }
|
|
|
|
|
|
|
|
it 'removes the terraform state lock' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with an incorrect lock id' do
|
2020-07-28 23:09:34 +05:30
|
|
|
let(:lock_id) { '456-789' }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
it 'returns an error' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:conflict)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with a longer than 255 character lock id' do
|
2020-07-28 23:09:34 +05:30
|
|
|
let(:lock_id) { '0' * 256 }
|
2020-04-22 19:07:51 +05:30
|
|
|
|
|
|
|
it 'returns an error' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
|
|
end
|
|
|
|
end
|
2020-07-28 23:09:34 +05:30
|
|
|
|
|
|
|
context 'user does not have permission to unlock the state' do
|
|
|
|
let(:lock_id) { '123-456' }
|
|
|
|
let(:current_user) { developer }
|
|
|
|
|
|
|
|
it 'returns an error' do
|
|
|
|
request
|
|
|
|
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
|
|
end
|
|
|
|
end
|
2020-04-22 19:07:51 +05:30
|
|
|
end
|
|
|
|
end
|