Commit 0b0d9e50 authored by allison.browne's avatar allison.browne

Have the publish attachments service not inherit

The attachment service does not need everything from the
base service so this removes the inheritance
Make total_uploads and instance variable
add comment to publish service
parent c967cf3e
# frozen_string_literal: true
module StatusPage
module PublicationServiceHelpers
include Gitlab::Utils::StrongMemoize
def error(message, payload = {})
ServiceResponse.error(message: message, payload: payload)
end
def success(payload = {})
ServiceResponse.success(payload: payload)
end
end
end
...@@ -2,62 +2,75 @@ ...@@ -2,62 +2,75 @@
module StatusPage module StatusPage
# Publishes Attachments from incident comments and descriptions to s3 # Publishes Attachments from incident comments and descriptions to s3
# Should only be called from publish details # Should only be called from publish details or a service that inherits from the publish_base_service
class PublishAttachmentsService < PublishBaseService class PublishAttachmentsService
private include StatusPage::PublicationServiceHelpers
def initialize(project:, issue:, user_notes:, storage_client:)
@project = project
@issue = issue
@user_notes = user_notes
@storage_client = storage_client
@total_uploads = existing_keys.size
end
def process(issue, user_notes) def execute
total_uploads = existing_keys(issue).size publish_description_attachments
publish_description_attachments(issue, total_uploads) publish_user_note_attachments
publish_user_note_attachments(issue, total_uploads, user_notes)
success success
end end
def publish_description_attachments(issue, total_uploads) private
attr_reader :project, :issue, :user_notes, :storage_client
def publish_description_attachments
publish_markdown_uploads( publish_markdown_uploads(
markdown_field: issue.description, markdown_field: issue.description,
issue_iid: issue.iid, issue_iid: issue.iid
total_uploads: total_uploads
) )
end end
def publish_user_note_attachments(issue, total_uploads, user_notes) def publish_user_note_attachments
user_notes.each do |user_note| user_notes.each do |user_note|
publish_markdown_uploads( publish_markdown_uploads(
markdown_field: user_note.note, markdown_field: user_note.note,
issue_iid: issue.iid, issue_iid: issue.iid
total_uploads: total_uploads
) )
end end
end end
def existing_keys(issue = nil) def publish_markdown_uploads(markdown_field:, issue_iid:)
strong_memoize(:existing_keys) do
storage_client.list_object_keys(
uploads_path(issue)
)
end
end
def uploads_path(issue)
StatusPage::Storage.uploads_path(issue.iid)
end
def publish_markdown_uploads(markdown_field:, issue_iid:, total_uploads:)
markdown_field.scan(FileUploader::MARKDOWN_PATTERN).map do |secret, file_name| markdown_field.scan(FileUploader::MARKDOWN_PATTERN).map do |secret, file_name|
break if total_uploads >= StatusPage::Storage::MAX_UPLOADS break if @total_uploads >= StatusPage::Storage::MAX_UPLOADS
key = StatusPage::Storage.upload_path(issue_iid, secret, file_name) key = StatusPage::Storage.upload_path(issue_iid, secret, file_name)
next if existing_keys.include? key next if existing_keys.include? key
uploader = UploaderFinder.new(@project, secret, file_name).execute # uploader behaves like a file with an 'open' method
uploader.open do |open_file| file = UploaderFinder.new(project, secret, file_name).execute
storage_client.multipart_upload(key, open_file)
total_uploads += 1 upload_file(key, file)
end
end end
end end
def upload_file(key, file)
file.open do |open_file|
# Send files to s3 storage in parts (hanles large files)
storage_client.multipart_upload(key, open_file)
@total_uploads += 1
end
end
def existing_keys
strong_memoize(:existing_keys) do
storage_client.list_object_keys(uploads_path)
end
end
def uploads_path
StatusPage::Storage.uploads_path(issue.iid)
end
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module StatusPage module StatusPage
class PublishBaseService class PublishBaseService
include Gitlab::Utils::StrongMemoize include StatusPage::PublicationServiceHelpers
def initialize(project:) def initialize(project:)
@project = project @project = project
...@@ -71,10 +71,6 @@ module StatusPage ...@@ -71,10 +71,6 @@ module StatusPage
.valid? .valid?
end end
def error(message, payload = {})
ServiceResponse.error(message: message, payload: payload)
end
def error_limit_exceeded(key) def error_limit_exceeded(key)
error("Failed to upload #{key}: Limit exceeded") error("Failed to upload #{key}: Limit exceeded")
end end
...@@ -86,9 +82,5 @@ module StatusPage ...@@ -86,9 +82,5 @@ module StatusPage
def error_no_storage_client def error_no_storage_client
error('No storage client available. Is the status page setting activated?') error('No storage client available. Is the status page setting activated?')
end end
def success(payload = {})
ServiceResponse.success(payload: payload)
end
end end
end end
...@@ -39,7 +39,12 @@ module StatusPage ...@@ -39,7 +39,12 @@ module StatusPage
end end
def publish_attachments(issue, user_notes) def publish_attachments(issue, user_notes)
StatusPage::PublishAttachmentsService.new(project: @project).execute(issue, user_notes) StatusPage::PublishAttachmentsService.new(
project: @project,
issue: issue,
user_notes: user_notes,
storage_client: storage_client
).execute
end end
end end
end end
...@@ -6,6 +6,7 @@ module StatusPage ...@@ -6,6 +6,7 @@ module StatusPage
# #
# Use this service for publishing an incident to CDN synchronously. # Use this service for publishing an incident to CDN synchronously.
# To publish asynchronously use +StatusPage::TriggerPublishService+ instead. # To publish asynchronously use +StatusPage::TriggerPublishService+ instead.
# Called within an async job so as to run out of out of band from web requests
# #
# This services calls: # This services calls:
# * StatusPage::PublishDetailsService # * StatusPage::PublishDetailsService
......
...@@ -70,7 +70,7 @@ module StatusPage ...@@ -70,7 +70,7 @@ module StatusPage
# key: s3 key at which file is stored # key: s3 key at which file is stored
# file: An open file or file-like io object # file: An open file or file-like io object
def multipart_upload(key, file) def multipart_upload(key, file)
StatusPage::Storage::S3Client.new( StatusPage::Storage::S3MultipartUpload.new(
client: client, bucket_name: bucket_name, key: key, open_file: file client: client, bucket_name: bucket_name, key: key, open_file: file
).call ).call
end end
......
...@@ -30,6 +30,7 @@ module StatusPage ...@@ -30,6 +30,7 @@ module StatusPage
complete_upload(upload_id, parts) complete_upload(upload_id, parts)
# Rescue on Exception since even on keyboard inturrupt we want to abort the upload and re-raise # Rescue on Exception since even on keyboard inturrupt we want to abort the upload and re-raise
# abort clears the already uploaded parts so that they do not cost the bucket owner # abort clears the already uploaded parts so that they do not cost the bucket owner
# The status page bucket lifecycle policy will clear out any parts if this fails without an exception (power failures etc.)
rescue Exception => e # rubocop:disable Lint/RescueException rescue Exception => e # rubocop:disable Lint/RescueException
abort_upload(upload_id) abort_upload(upload_id)
raise e raise e
......
...@@ -3,33 +3,33 @@ ...@@ -3,33 +3,33 @@
require 'spec_helper' require 'spec_helper'
describe StatusPage::PublishAttachmentsService do describe StatusPage::PublishAttachmentsService do
let_it_be(:project, refind: true) { create(:project) } RSpec.shared_context 'second file' do
let(:markdown_field) { 'Hello World' } # Setup second file
let(:user_notes) { [] } let(:upload_secret_2) { '9cb61a79ce884d5b681dd42728d3c159' }
let(:incident_id) { 1 } let(:image_file_name_2) { 'tanuki_2.png' }
let(:issue) { instance_double(Issue, notes: user_notes, description: markdown_field, iid: incident_id) } let(:upload_path_2) { "/uploads/#{upload_secret_2}/#{image_file_name_2}" }
let(:key) { StatusPage::Storage.details_path(incident_id) } let(:markdown_field) { "![tanuki](#{upload_path}) and ![tanuki_2](#{upload_path_2})" }
let(:content) { { id: incident_id } } let(:status_page_upload_path_2) { StatusPage::Storage.upload_path(issue.iid, upload_secret_2, image_file_name_2) }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
let(:status_page_setting_enabled) { true }
let(:status_page_setting) do
instance_double(StatusPage::ProjectSetting, enabled?: status_page_setting_enabled,
storage_client: storage_client)
end end
let(:service) { described_class.new(project: project) } describe '#execute' do
let_it_be(:project, refind: true) { create(:project) }
let(:markdown_field) { 'Hello World' }
let(:user_notes) { [] }
let(:incident_id) { 1 }
let(:issue) { instance_double(Issue, notes: user_notes, description: markdown_field, iid: incident_id) }
let(:key) { StatusPage::Storage.details_path(incident_id) }
let(:content) { { id: incident_id } }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
subject(:result) { service.execute(issue, user_notes) } let(:service) { described_class.new(project: project, issue: issue, user_notes: user_notes, storage_client: storage_client) }
before do subject { service.execute }
allow(project).to receive(:status_page_setting)
.and_return(status_page_setting) include_context 'stub status page enabled'
end
describe '#execute' do
context 'publishes file attachments' do context 'publishes file attachments' do
before do before do
stub_licensed_features(status_page: true)
allow(storage_client).to receive(:upload_object).with("data/incident/1.json", "{\"id\":1}") allow(storage_client).to receive(:upload_object).with("data/incident/1.json", "{\"id\":1}")
allow(storage_client).to receive(:list_object_keys).and_return(Set.new) allow(storage_client).to receive(:list_object_keys).and_return(Set.new)
end end
...@@ -37,8 +37,8 @@ describe StatusPage::PublishAttachmentsService do ...@@ -37,8 +37,8 @@ describe StatusPage::PublishAttachmentsService do
context 'when not in markdown' do context 'when not in markdown' do
it 'publishes no images' do it 'publishes no images' do
expect(storage_client).not_to receive(:multipart_upload) expect(storage_client).not_to receive(:multipart_upload)
expect(result.payload).to eq({}) expect(subject.payload).to eq({})
expect(result).to be_success expect(subject).to be_success
end end
end end
...@@ -67,8 +67,8 @@ describe StatusPage::PublishAttachmentsService do ...@@ -67,8 +67,8 @@ describe StatusPage::PublishAttachmentsService do
it 'publishes description images' do it 'publishes description images' do
expect(storage_client).to receive(:multipart_upload).with(status_page_upload_path, open_file).once expect(storage_client).to receive(:multipart_upload).with(status_page_upload_path, open_file).once
expect(result).to be_success expect(subject).to be_success
expect(result.payload).to eq({}) expect(subject.payload).to eq({})
end end
context 'user notes uploads' do context 'user notes uploads' do
...@@ -79,22 +79,24 @@ describe StatusPage::PublishAttachmentsService do ...@@ -79,22 +79,24 @@ describe StatusPage::PublishAttachmentsService do
it 'publishes images' do it 'publishes images' do
expect(storage_client).to receive(:multipart_upload).with(status_page_upload_path, open_file).once expect(storage_client).to receive(:multipart_upload).with(status_page_upload_path, open_file).once
expect(result).to be_success expect(subject).to be_success
expect(result.payload).to eq({}) expect(subject.payload).to eq({})
end end
end end
context 'when exceeds upload limit' do context 'when exceeds upload limit' do
include_context 'second file'
before do before do
stub_const("StatusPage::Storage::MAX_UPLOADS", 1) stub_const("StatusPage::Storage::MAX_UPLOADS", 2)
allow(storage_client).to receive(:list_object_keys).and_return(Set[status_page_upload_path]) allow(storage_client).to receive(:list_object_keys).and_return(Set['existing_key'])
end end
it 'publishes no images' do it 'publishes no images' do
expect(storage_client).not_to receive(:multipart_upload) expect(storage_client).to receive(:multipart_upload).once
expect(result).to be_success expect(subject).to be_success
expect(result.payload).to eq({}) expect(subject.payload).to eq({})
end end
end end
...@@ -106,17 +108,13 @@ describe StatusPage::PublishAttachmentsService do ...@@ -106,17 +108,13 @@ describe StatusPage::PublishAttachmentsService do
it 'publishes no images' do it 'publishes no images' do
expect(storage_client).not_to receive(:multipart_upload) expect(storage_client).not_to receive(:multipart_upload)
expect(result).to be_success expect(subject).to be_success
expect(result.payload).to eq({}) expect(subject.payload).to eq({})
end end
end end
context 'when images are already in s3' do context 'when images are already in s3' do
let(:upload_secret_2) { '9cb61a79ce884d5b681dd42728d3c159' } include_context 'second file'
let(:image_file_name_2) { 'tanuki_2.png' }
let(:upload_path_2) { "/uploads/#{upload_secret_2}/#{image_file_name_2}" }
let(:markdown_field) { "![tanuki](#{upload_path}) and ![tanuki_2](#{upload_path_2})" }
let(:status_page_upload_path_2) { StatusPage::Storage.upload_path(issue.iid, upload_secret_2, image_file_name_2) }
before do before do
allow(storage_client).to receive(:list_object_keys).and_return(Set[status_page_upload_path]) allow(storage_client).to receive(:list_object_keys).and_return(Set[status_page_upload_path])
...@@ -126,8 +124,8 @@ describe StatusPage::PublishAttachmentsService do ...@@ -126,8 +124,8 @@ describe StatusPage::PublishAttachmentsService do
expect(storage_client).to receive(:multipart_upload).with(status_page_upload_path_2, open_file).once expect(storage_client).to receive(:multipart_upload).with(status_page_upload_path_2, open_file).once
expect(storage_client).not_to receive(:multipart_upload).with(status_page_upload_path, open_file) expect(storage_client).not_to receive(:multipart_upload).with(status_page_upload_path, open_file)
expect(result).to be_success expect(subject).to be_success
expect(result.payload).to eq({}) expect(subject.payload).to eq({})
end end
end end
end end
......
...@@ -4,10 +4,9 @@ require 'spec_helper' ...@@ -4,10 +4,9 @@ require 'spec_helper'
describe StatusPage::PublishDetailsService do describe StatusPage::PublishDetailsService do
let_it_be(:project, refind: true) { create(:project) } let_it_be(:project, refind: true) { create(:project) }
let(:markdown_field) { 'Hello World' }
let(:user_notes) { [] } let(:user_notes) { [] }
let(:incident_id) { 1 } let(:incident_id) { 1 }
let(:issue) { instance_double(Issue, notes: user_notes, description: markdown_field, iid: incident_id) } let(:issue) { instance_double(Issue, notes: user_notes, description: 'Incident Occuring', iid: incident_id) }
let(:key) { StatusPage::Storage.details_path(incident_id) } let(:key) { StatusPage::Storage.details_path(incident_id) }
let(:content) { { id: incident_id } } let(:content) { { id: incident_id } }
...@@ -35,9 +34,10 @@ describe StatusPage::PublishDetailsService do ...@@ -35,9 +34,10 @@ describe StatusPage::PublishDetailsService do
context 'publishes attachments' do context 'publishes attachments' do
it 'sends attachements to s3' do it 'sends attachements to s3' do
allow(storage_client).to receive(:upload_object) allow(storage_client).to receive(:upload_object)
allow(storage_client).to receive(:list_object_keys).and_return([])
expect_next_instance_of(StatusPage::PublishAttachmentsService) do |service| expect_next_instance_of(StatusPage::PublishAttachmentsService) do |service|
expect(service).to receive(:execute).with(issue, user_notes) expect(service).to receive(:execute)
end end
subject subject
......
...@@ -12,3 +12,20 @@ RSpec.shared_context 'status page enabled' do ...@@ -12,3 +12,20 @@ RSpec.shared_context 'status page enabled' do
end end
end end
end end
RSpec.shared_context 'stub status page enabled' do
let(:status_page_setting_enabled) { true }
let(:status_page_setting) do
instance_double(
StatusPage::ProjectSetting,
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
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