Commit c4409050 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'pl-status-page-mvc-publish-service' into 'master'

Implement publish incident service

See merge request gitlab-org/gitlab!26785
parents fdc3b26c e32f7d29
......@@ -41,6 +41,17 @@ class StatusPageSetting < ApplicationRecord
project&.beta_feature_available?(:status_page)
end
def storage_client
return unless enabled?
StatusPage::Storage::S3Client.new(
region: aws_region,
bucket_name: aws_s3_bucket_name,
access_key_id: aws_access_key,
secret_access_key: aws_secret_key
)
end
private
def check_secret_changes
......
......@@ -2,28 +2,44 @@
module StatusPage
class PublishBaseService
JSON_MAX_SIZE = 1.megabyte
include Gitlab::Utils::StrongMemoize
def initialize(project:, storage_client:, serializer:)
def initialize(project:)
@project = project
@storage_client = storage_client
@serializer = serializer
end
def execute(*args)
return error_feature_not_available unless feature_available?
return error_no_storage_client unless storage_client
publish(*args)
end
private
attr_reader :project, :storage_client, :serializer
attr_reader :project
def publish(*args)
raise NotImplementedError
end
def storage_client
strong_memoize(:strong_memoize) do
project.status_page_setting&.storage_client
end
end
def serializer
strong_memoize(:serializer) do
# According to development/reusing_abstractions.html#abstractions
# serializers can only be used from controllers.
# For the Status Page however, we generate JSON in background jobs.
# rubocop: disable CodeReuse/Serializer
StatusPage::IncidentSerializer.new
# rubocop: enable CodeReuse/Serializer
end
end
def feature_available?
project.status_page_setting&.enabled?
end
......@@ -35,12 +51,12 @@ module StatusPage
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?
!Gitlab::Utils::DeepSize
.new(json, max_size: Storage::JSON_MAX_SIZE)
.valid?
end
def error(message, payload = {})
......@@ -55,6 +71,10 @@ module StatusPage
error('Feature not available')
end
def error_no_storage_client
error('No storage client available. Is the status page setting activated?')
end
def success(payload = {})
ServiceResponse.success(payload: payload)
end
......
# frozen_string_literal: true
module StatusPage
# Render an issue as incident details and publish them to 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 PublishDetailsService < PublishBaseService
private
......
# frozen_string_literal: true
module StatusPage
# Delegate work to more specific publishing services.
#
# Use this service for publishing an incident to CDN which calls:
# * StatusPage::PublishDetailsService
# * StatusPage::PublishListService
class PublishIncidentService
include Gitlab::Utils::StrongMemoize
def initialize(project:, issue_id:)
@project = project
@issue_id = issue_id
end
def execute
return error_issue_not_found unless issue
response = publish_details
return response if response.error?
publish_list
end
private
attr_reader :project, :issue_id
def publish_details
PublishDetailsService.new(project: project).execute(issue, user_notes)
end
def publish_list
PublishListService.new(project: project).execute(issues)
end
def issue
strong_memoize(:issue) { issues_finder.find_by_id(issue_id) }
end
def user_notes
strong_memoize(:user_notes) do
IncidentCommentsFinder.new(issue: issue).all
end
end
def issues
strong_memoize(:issues) { issues_finder.all }
end
def issues_finder
strong_memoize(:issues_finder) do
IncidentsFinder.new(project_id: project.id)
end
end
def error_issue_not_found
ServiceResponse.error(message: 'Issue not found')
end
end
end
# frozen_string_literal: true
module StatusPage
# Render a list of issues as incidents and publish them to 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 PublishListService < PublishBaseService
private
......
......@@ -2,6 +2,8 @@
module StatusPage
module Storage
# Size limit of the generated JSON uploaded to CDN.
JSON_MAX_SIZE = 1.megabyte
# Limit the amount of the recent incidents in the JSON list
MAX_RECENT_INCIDENTS = 20
# Limit the amount of comments per incident
......
......@@ -109,4 +109,27 @@ describe StatusPageSetting do
it { is_expected.to eq(false) }
end
end
describe '#storage_client' do
let(:status_page_setting) { build(:status_page_setting, :enabled) }
before do
allow(status_page_setting)
.to receive(:enabled?).and_return(status_page_setting_enabled)
end
subject { status_page_setting.storage_client }
context 'when status page settings is enabled' do
let(:status_page_setting_enabled) { true }
it { is_expected.to be_instance_of(StatusPage::Storage::S3Client) }
end
context 'when not enabled' do
let(:status_page_setting_enabled) { false }
it { is_expected.to be_nil }
end
end
end
......@@ -4,39 +4,30 @@ 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
let(:service) { described_class.new(project: project) }
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
before do
allow(serializer).to receive(:represent_details).with(issue, user_notes)
.and_return(content)
end
include_examples 'publish incidents'
include_examples 'publish incidents'
context 'when serialized content is missing id' do
let(:content) { { other_id: incident_id } }
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
it 'returns an error' do
expect(result).to be_error
expect(result.message).to eq('Missing object key')
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::PublishIncidentService do
let_it_be(:project, refind: true) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:settings) { create(:status_page_setting, :enabled, project: project) }
let(:service) { described_class.new(project: project, issue_id: issue.id) }
subject(:result) { service.execute }
describe '#execute' do
before do
stub_licensed_features(status_page: true)
end
context 'when publishing succeeds' do
it 'returns uploads incidents details and list' do
expect_to_upload_details(issue)
expect_to_upload_list
expect(result).to be_success
end
end
context 'when uploading details fails' do
it 'propagates the exception' do
expect_to_upload_details(issue, status: 404)
expect { result }.to raise_error(StatusPage::Storage::Error)
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)
expect { result }.to raise_error(StatusPage::Storage::Error)
end
end
context 'with unrelated issue' do
let(:issue) { create(:issue) }
it 'returns error issue not found' do
expect(result).to be_error
expect(result.message).to eq('Issue not found')
end
end
end
private
def expect_to_upload_details(issue, **kwargs)
stub_upload_request(StatusPage::Storage.details_path(issue.iid), **kwargs)
end
def expect_to_upload_list(**kwargs)
stub_upload_request(StatusPage::Storage.list_path, **kwargs)
end
def stub_upload_request(path, status: 200)
stub_request(:put, %r{amazonaws.com/#{path}}).to_return(status: status)
end
end
......@@ -4,29 +4,20 @@ 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
let(:service) { described_class.new(project: project) }
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'
before do
allow(serializer).to receive(:represent_list).with(issues)
.and_return(content)
end
include_examples 'publish incidents'
end
end
# frozen_string_literal: true
RSpec.shared_examples 'publish incidents' do
let_it_be(:status_page_setting) do
create(:status_page_setting, :enabled, project: project)
let(:status_page_setting_enabled) { true }
let(:storage_client) { instance_double(StatusPage::Storage::S3Client) }
let(:serializer) { instance_double(StatusPage::IncidentSerializer) }
let(:content_json) { content.to_json }
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)
allow(StatusPage::IncidentSerializer).to receive(:new)
.and_return(serializer)
end
shared_examples 'feature is not available' do
it 'returns feature not available error' do
expect(result).to be_error
expect(result.message).to eq('Feature not available')
end
end
context 'when upload succeeds' do
......@@ -40,15 +47,13 @@ RSpec.shared_examples 'publish incidents' do
.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)
it 'propagates the exception' do
expect { result }.to raise_error(exception)
end
end
context 'when limits exceeded' do
let(:too_big) { 'a' * StatusPage::PublishBaseService::JSON_MAX_SIZE }
let(:too_big) { 'a' * StatusPage::Storage::JSON_MAX_SIZE }
before do
if content.is_a?(Array)
......@@ -66,27 +71,12 @@ RSpec.shared_examples 'publish incidents' do
end
end
context 'when feature is not available' do
before do
stub_licensed_features(status_page: false)
end
it_behaves_like 'feature is not available'
end
context 'when status page setting is disabled' do
before do
status_page_setting.update!(enabled: false)
end
it_behaves_like 'feature is not available'
end
context 'when status page setting is not enabled' do
let(:status_page_setting_enabled) { false }
context 'when feature flag is disabled' do
before do
stub_feature_flags(status_page: false)
it 'returns feature not available error' do
expect(result).to be_error
expect(result.message).to eq('Feature not available')
end
it_behaves_like 'feature is not available'
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