Commit 59d90368 authored by Peter Leitzen's avatar Peter Leitzen

Unpublish details for confidential issues

* Implement deleting objects for S3 client
* Implement unpublishing service
* Rename publish to process
* Allow finding a confidential issue for status page
parent 6e96907b
......@@ -10,10 +10,10 @@
#
# finder = StatusPage::IncidentsFinder.new(project_id: project_id)
#
# # A single issue
# issue, user_notes = finder.find_by_id(issue_id)
# # A single issue (including confidential issue)
# issue = finder.find_by_id(issue_id)
#
# # Most recent 20 issues
# # Most recent 20 non-confidential issues
# issues = finder.all
#
module StatusPage
......@@ -25,23 +25,22 @@ module StatusPage
end
def find_by_id(issue_id)
execute.find_by_id(issue_id)
execute(include_confidential: true)
.find_by_id(issue_id)
end
def all
@sorted = true
execute
execute(sorted: true)
.limit(MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
end
private
attr_reader :project_id, :with_user_notes, :sorted
attr_reader :project_id
def execute
def execute(sorted: false, include_confidential: false)
issues = init_collection
issues = public_only(issues)
issues = public_only(issues) unless include_confidential
issues = by_project(issues)
issues = reverse_chronological(issues) if sorted
issues
......
......@@ -12,14 +12,14 @@ module StatusPage
return error_feature_not_available unless feature_available?
return error_no_storage_client unless storage_client
publish(*args)
process(*args)
end
private
attr_reader :project
def publish(*args)
def process(*args)
raise NotImplementedError
end
......@@ -53,6 +53,12 @@ module StatusPage
success(object_key: key)
end
def delete(key)
storage_client.delete_object(key)
success(object_key: key)
end
def limit_exceeded?(json)
!Gitlab::Utils::DeepSize
.new(json, max_size: Storage::JSON_MAX_SIZE)
......
......@@ -10,7 +10,7 @@ module StatusPage
class PublishDetailsService < PublishBaseService
private
def publish(issue, user_notes)
def process(issue, user_notes)
json = serialize(issue, user_notes)
key = object_key(json)
return error('Missing object key') unless key
......
......@@ -9,6 +9,7 @@ module StatusPage
#
# This services calls:
# * StatusPage::PublishDetailsService
# * StatusPage::UnpublishDetailsService
# * StatusPage::PublishListService
class PublishIncidentService
include Gitlab::Utils::StrongMemoize
......@@ -23,22 +24,34 @@ module StatusPage
return error_permission_denied unless can_publish?
return error_issue_not_found unless issue
response = publish_details
response = process_details
return response if response.error?
publish_list
process_list
end
private
attr_reader :user, :project, :issue_id
def process_details
if issue.confidential?
unpublish_details
else
publish_details
end
end
def process_list
PublishListService.new(project: project).execute(issues)
end
def publish_details
PublishDetailsService.new(project: project).execute(issue, user_notes)
end
def publish_list
PublishListService.new(project: project).execute(issues)
def unpublish_details
UnpublishDetailsService.new(project: project).execute(issue)
end
def issue
......
......@@ -10,7 +10,7 @@ module StatusPage
class PublishListService < PublishBaseService
private
def publish(issues)
def process(issues)
json = serialize(issues)
upload(object_key, json)
......
# frozen_string_literal: true
module StatusPage
# Unpublish incident details from CDN.
#
# Example: An issue becomes confidential so it must be removed from CDN.
#
# This is an internal service which is part of
# +StatusPage::PublishIncidentService+ and is not meant to be called directly.
#
# Consider calling +StatusPage::PublishIncidentService+ instead.
class UnpublishDetailsService < PublishBaseService
private
def process(issue)
key = object_key(issue)
delete(key)
end
def object_key(issue)
StatusPage::Storage.details_path(issue.iid)
end
end
end
......@@ -25,6 +25,17 @@ module StatusPage
true
end
# Deletes +key+ from storage
#
# Note, this operation succeeds even if +key+ does not exist in storage.
def delete_object(key)
wrap_errors(key: key) do
client.delete_object(bucket: bucket_name, key: key)
end
true
end
private
attr_reader :client, :bucket_name
......
......@@ -28,7 +28,7 @@ describe StatusPage::IncidentsFinder do
context 'for confidential issue' do
let(:issue) { issues.fetch(:confidential) }
it { is_expected.to be_nil }
it { is_expected.to eq(issue) }
end
context 'for unrelated issue' do
......@@ -40,6 +40,7 @@ describe StatusPage::IncidentsFinder do
describe '#all' do
let(:sorted_issues) { public_issues.sort_by(&:created_at).reverse }
let(:limit) { public_issues.size }
subject { finder.all }
......@@ -58,5 +59,13 @@ describe StatusPage::IncidentsFinder do
it { is_expected.to eq(sorted_issues.first(1)) }
end
context 'when combined with other finder methods' do
before do
finder.find_by_id(public_issues.first.id)
end
it { is_expected.to eq(sorted_issues) }
end
end
end
......@@ -41,6 +41,29 @@ describe StatusPage::Storage::S3Client, :aws_s3 do
end
end
describe '#delete_object' do
let(:key) { 'key' }
subject(:result) { client.delete_object(key) }
it 'returns true' do
stub_responses(:delete_object)
expect(result).to eq(true)
end
context 'when failed' do
let(:aws_error) { 'SomeError' }
it 'raises an error' do
stub_responses(:delete_object, aws_error)
msg = error_message(aws_error, key: key)
expect { result }.to raise_error(StatusPage::Storage::Error, msg)
end
end
end
private
def stub_responses(*args)
......
......@@ -23,29 +23,54 @@ describe StatusPage::PublishIncidentService do
.and_return(user_can_publish)
end
context 'when publishing succeeds' do
it 'returns uploads incidents details and list' do
expect_to_upload_details(issue)
expect_to_upload_list
describe 'publish details' do
context 'when upload succeeds' do
it 'uploads incident details and list' do
expect_to_upload_details(issue)
expect_to_upload_list
expect(result).to be_success
end
end
context 'when upload fails' do
it 'propagates the exception' do
expect_to_upload_details(issue, status: 404)
expect(result).to be_success
expect { result }.to raise_error(StatusPage::Storage::Error)
end
end
end
context 'when uploading details fails' do
it 'propagates the exception' do
expect_to_upload_details(issue, status: 404)
describe 'unpublish details' do
let_it_be(:issue) { create(:issue, :confidential, project: project) }
context 'when deletion succeeds' do
it 'deletes incident details and upload list' do
expect_to_delete_details(issue)
expect_to_upload_list
expect(result).to be_success
end
end
context 'when deletion fails' do
it 'propagates the exception' do
expect_to_delete_details(issue, status: 403)
expect { result }.to raise_error(StatusPage::Storage::Error)
expect { result }.to raise_error(StatusPage::Storage::Error)
end
end
end
context 'when uploading list fails' do
it 'returns error and skip list upload' do
expect_to_upload_details(issue)
expect_to_upload_list(status: 404)
describe 'publish list' do
context 'when upload fails' do
it 'returns error and skip list upload' do
expect_to_upload_details(issue)
expect_to_upload_list(status: 404)
expect { result }.to raise_error(StatusPage::Storage::Error)
expect { result }.to raise_error(StatusPage::Storage::Error)
end
end
end
......@@ -71,14 +96,18 @@ describe StatusPage::PublishIncidentService do
private
def expect_to_upload_details(issue, **kwargs)
stub_upload_request(StatusPage::Storage.details_path(issue.iid), **kwargs)
stub_aws_request(:put, StatusPage::Storage.details_path(issue.iid), **kwargs)
end
def expect_to_delete_details(issue, **kwargs)
stub_aws_request(:delete, StatusPage::Storage.details_path(issue.iid), **kwargs)
end
def expect_to_upload_list(**kwargs)
stub_upload_request(StatusPage::Storage.list_path, **kwargs)
stub_aws_request(:put, StatusPage::Storage.list_path, **kwargs)
end
def stub_upload_request(path, status: 200)
stub_request(:put, %r{amazonaws.com/#{path}}).to_return(status: status)
def stub_aws_request(method, path, status: 200)
stub_request(method, %r{amazonaws.com/#{path}}).to_return(status: status)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::UnpublishDetailsService do
let_it_be(:project, refind: true) { create(:project) }
let(:issue) { instance_double(Issue, iid: incident_id) }
let(:incident_id) { 1 }
let(:key) { StatusPage::Storage.details_path(incident_id) }
let(:service) { described_class.new(project: project) }
subject(:result) { service.execute(issue) }
describe '#execute' do
let(:status_page_setting_enabled) { true }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
let(:status_page_setting) do
instance_double(StatusPageSetting, enabled?: status_page_setting_enabled,
storage_client: storage_client)
end
before do
stub_licensed_features(status_page: true)
allow(project).to receive(:status_page_setting)
.and_return(status_page_setting)
end
context 'when deletion succeeds' do
before do
allow(storage_client).to receive(:delete_object).with(key)
end
it 'removes details from CDN' do
expect(result).to be_success
expect(result.payload).to eq(object_key: key)
end
end
context 'when upload fails due to exception' do
let(:bucket) { 'bucket_name' }
let(:error) { StandardError.new }
let(:exception) do
StatusPage::Storage::Error.new(bucket: bucket, error: error)
end
before do
allow(storage_client).to receive(:delete_object).with(key)
.and_raise(exception)
end
it 'propagates the exception' do
expect { result }.to raise_error(exception)
end
end
context 'when status page setting is not enabled' do
let(:status_page_setting_enabled) { false }
it 'returns feature not available error' do
expect(result).to be_error
expect(result.message).to eq('Feature not available')
end
end
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