Commit 9576062b authored by Peter Leitzen's avatar Peter Leitzen Committed by Bob Van Landuyt

Publish incident details and list to status page

This commit implements two services which serialize issues and uploads
generated JSON to S3.
parent 8cc37ed4
......@@ -126,6 +126,7 @@ class License < ApplicationRecord
report_approver_rules
sast
security_dashboard
status_page
subepics
threat_monitoring
tracing
......
# frozen_string_literal: true
module StatusPage
class PublishBaseService
JSON_MAX_SIZE = 1.megabyte
def initialize(project:, storage_client:, serializer:)
@project = project
@storage_client = storage_client
@serializer = serializer
end
def execute(*args)
return error_feature_not_available unless feature_available?
publish(*args)
end
private
attr_reader :project, :storage_client, :serializer
def publish(*args)
raise NotImplementedError
end
def feature_available?
project.feature_available?(:status_page)
end
def upload(key, json)
return error_limit_exceeded(key) if limit_exceeded?(json)
content = json.to_json
storage_client.upload_object(key, content)
success(object_key: key)
rescue StatusPage::Storage::Error => e
error(e.message, error: e)
end
def limit_exceeded?(json)
!Gitlab::Utils::DeepSize.new(json, max_size: JSON_MAX_SIZE).valid?
end
def error(message, payload = {})
ServiceResponse.error(message: message, payload: payload)
end
def error_limit_exceeded(key)
error("Failed to upload #{key}: Limit exceeded")
end
def error_feature_not_available
error('Feature not available')
end
def success(payload = {})
ServiceResponse.success(payload: payload)
end
end
end
# frozen_string_literal: true
module StatusPage
class PublishDetailsService < PublishBaseService
private
def publish(issue, user_notes)
json = serialize(issue, user_notes)
key = object_key(json)
return error('Missing object key') unless key
upload(key, json)
end
def serialize(issue, user_notes)
serializer.represent_details(issue, user_notes)
end
def object_key(json)
id = json[:id]
return unless id
StatusPage::Storage.details_path(id)
end
end
end
# frozen_string_literal: true
module StatusPage
class PublishListService < PublishBaseService
private
def publish(issues)
json = serialize(issues)
upload(object_key, json)
end
def serialize(issues)
serializer.represent_list(issues)
end
def object_key
StatusPage::Storage.list_path
end
end
end
......@@ -2,6 +2,14 @@
module StatusPage
module Storage
def self.details_path(id)
"incident/#{id}.json"
end
def self.list_path
'list.json'
end
class Error < StandardError
def initialize(bucket:, error:, **args)
super(
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::Storage do
describe '.details_path' do
subject { described_class.details_path(123) }
it { is_expected.to eq('incident/123.json') }
end
describe '.list_path' do
subject { described_class.list_path }
it { is_expected.to eq('list.json') }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::PublishDetailsService do
let_it_be(:project, refind: true) { create(:project) }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
let(:serializer) { instance_double(StatusPage::IncidentSerializer) }
let(:issue) { instance_double(Issue) }
let(:user_notes) { double(:user_notes) }
let(:incident_id) { 1 }
let(:key) { StatusPage::Storage.details_path(incident_id) }
let(:content) { { id: incident_id } }
let(:content_json) { content.to_json }
let(:service) do
described_class.new(
project: project, storage_client: storage_client, serializer: serializer
)
end
subject(:result) { service.execute(issue, user_notes) }
describe '#execute' do
context 'when license is available' do
before do
allow(serializer).to receive(:represent_details).with(issue, user_notes)
.and_return(content)
end
include_examples 'publish incidents'
context 'when serialized content is missing id' do
let(:content) { { other_id: incident_id } }
it 'returns an error' do
expect(result).to be_error
expect(result.message).to eq('Missing object key')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::PublishListService do
let_it_be(:project, refind: true) { create(:project) }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
let(:serializer) { instance_double(StatusPage::IncidentSerializer) }
let(:issues) { [instance_double(Issue)] }
let(:key) { StatusPage::Storage.list_path }
let(:content) { [{ some: :content }] }
let(:content_json) { content.to_json }
let(:service) do
described_class.new(
project: project, storage_client: storage_client, serializer: serializer
)
end
subject(:result) { service.execute(issues) }
describe '#execute' do
context 'when license is available' do
before do
allow(serializer).to receive(:represent_list).with(issues)
.and_return(content)
end
include_examples 'publish incidents'
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'publish incidents' do
before do
stub_licensed_features(status_page: true)
end
context 'when upload succeeds' do
before do
allow(storage_client).to receive(:upload_object).with(key, content_json)
end
it 'publishes details as JSON' 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(:upload_object).with(key, content_json)
.and_raise(exception)
end
it 'returns an error with exception' do
expect(result).to be_error
expect(result.message).to eq(exception.message)
expect(result.payload).to eq(error: exception)
end
end
context 'when limits exceeded' do
let(:too_big) { 'a' * StatusPage::PublishBaseService::JSON_MAX_SIZE }
before do
if content.is_a?(Array)
content.concat([too_big: too_big])
else
content.merge!(too_big: too_big)
end
end
it 'returns limit exceeded error' do
expect(result).to be_error
expect(result.message).to eq(
"Failed to upload #{key}: Limit exceeded"
)
end
end
context 'when feature is not available' do
before do
stub_licensed_features(status_page: false)
end
it 'returns feature not available error' do
expect(result).to be_error
expect(result.message).to eq('Feature not available')
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