3scale/porta

View on GitHub
app/models/service.rb

Summary

Maintainability
D
2 days
Test Coverage
# frozen_string_literal: true
 
File `service.rb` has 442 lines of code (exceeds 250 allowed). Consider refactoring.
require 'backend_client'
 
Class `Service` has 63 methods (exceeds 20 allowed). Consider refactoring.
Service has at least 62 methods
Service assumes too much for instance variable '@deleted_by_state_machine'
class Service < ApplicationRecord # rubocop:disable Metrics/ClassLength
include Searchable
include Backend::ModelExtensions::Service
include Logic::Contracting::Service
include Logic::PlanChanges::Service
include Logic::Authentication::Service
include Logic::RollingUpdates::Service
include BackendApiLogic::ServiceExtension
include SystemName
extend System::Database::Scopes::IdOrSystemName
include ServiceDiscovery::ModelExtensions::Service
include ProxyConfigAffectingChanges::ModelExtension
 
define_proxy_config_affecting_attributes :backend_version
 
self.background_deletion = [
:service_plans,
:application_plans,
[:api_docs_services, class_name: 'ApiDocs::Service'],
:backend_api_configs,
:metrics,
[:proxy, { action: :destroy, has_many: false }]
].freeze
 
DELETE_STATE = 'deleted'
 
has_system_name uniqueness_scope: :account_id
 
attr_readonly :system_name
 
validates :backend_version, inclusion: { in: ->(service) { BackendVersion.usable_versions(service: service) }}
 
include Authentication
validates :support_email, format: { with: RE_EMAIL_OK, message: MSG_EMAIL_BAD,
allow_blank: true }
 
validates :kubernetes_service_link, length: {maximum: 255}
 
after_create :create_default_metrics, :create_default_service_plan, :create_default_proxy
after_commit :update_notification_settings, if: :saved_change_to_notification_settings?
 
after_commit :create_and_publish_service_created_event, on: :create
after_commit :create_and_publish_service_deleted_event, on: :destroy
 
after_save :publish_events
after_save :deleted_without_state_machine
 
before_destroy :stop_destroy_if_last_or_default
after_destroy :update_account_default_service
 
after_commit :archive_as_deleted, on: :destroy
 
with_options(dependent: :destroy, inverse_of: :service) do |service|
service.has_many :service_plans, as: :issuer, &DefaultPlanProxy
service.has_many :application_plans, as: :issuer, &DefaultPlanProxy
service.has_many :api_docs_services, class_name: 'ApiDocs::Service'
end
 
sifter :of_account do |account|
account_id == account.id
end
 
scope :of_account, ->(account) { where.has { sift(:of_account, account) } }
 
has_one :proxy, dependent: :destroy, inverse_of: :service, autosave: true
 
belongs_to :default_service_plan, class_name: 'ServicePlan'
belongs_to :default_application_plan, class_name: 'ApplicationPlan'
 
attr_accessible is recommended over attr_protected
attr_protected :account_id, :tenant_id, :audit_ids
 
# LEGACY
alias plans application_plans
 
has_many :issued_plans, as: :issuer, class_name: 'Plan'
 
# for consistency with Provider
alias provided_plans issued_plans
 
has_many :cinstances, inverse_of: :service
has_many :service_contracts, through: :service_plans #, :readonly => true
 
has_many :contracts, through: :issued_plans do #, :readonly => true
# rename or remove - does not return the expected thing
def service
by_type :ServiceContract
end
 
def cinstance
by_type :Cinstance
end
 
alias_method :application, :cinstance
end
 
belongs_to :account
alias provider account
 
has_many :usage_limits, through: :application_plans
 
has_many :features, as: :featurable, dependent: :destroy
 
has_many :metrics, as: :owner, inverse_of: :owner, dependent: :destroy
has_many :top_level_metrics, -> { includes(:children).top_level }, as: :owner, class_name: 'Metric'
 
has_many :service_tokens, inverse_of: :service, dependent: :destroy
 
scope :accessible, -> { where.not(state: DELETE_STATE) }
scope :deleted, -> { where(state: DELETE_STATE) }
scope :of_approved_accounts, -> { joins(:account).merge(Account.approved) }
scope :permitted_for, ->(user = nil) {
next all unless user
 
permitted_services_status = user.permitted_services_status
 
next none if permitted_services_status == :none
 
account = user.account
account_services = (account.provider? ? account : account.provider_account).services
merge(
account_services.merge(permitted_services_status == :selected ? where(id: user.member_permission_service_ids) : {})
)
}
 
validates :name, presence: true
 
validates :name, :logo_file_name, :logo_content_type, :state,
:buyer_plan_change_permission, :system_name, :backend_version, :support_email, :deployment_option,
length: { maximum: 255 }
validates :terms, :notification_settings, :description, :txt_support, length: { maximum: 65535 }
 
accepts_nested_attributes_for :proxy
 
class DeploymentOption
PLUGINS = %i(ruby java python nodejs php rest csharp).freeze
private_constant :PLUGINS
 
APICAST = %i(hosted self_managed).freeze
private_constant :APICAST
 
SERVICE_MESH = %i[istio].freeze
private_constant :SERVICE_MESH
 
def self.plugins
PLUGINS.map { |lang| "plugin_#{lang}" }
end
 
def self.gateways
APICAST.map { |gateway| gateway.to_s.freeze }
end
 
def self.service_mesh
SERVICE_MESH.map { |name| "service_mesh_#{name}" }
end
 
def self.all
plugins + gateways + service_mesh
end
end
 
validates :deployment_option, inclusion: { in: DeploymentOption.all }, presence: true
scope :deployed_with_gateway, -> { where(deployment_option: DeploymentOption.gateways) }
 
serialize :notification_settings
 
annotated
audited
 
state_machine initial: :incomplete do
state :incomplete
state :hidden
state :offline
state :published
state :deprecated # (soft) scheduled for deletion
state :deleted # (hard) DELETED
 
event :take_offline do
transition published: :offline
end
 
# This is effectively the same as hide!, but the name is more descriptive.
event :complete do
transition incomplete: :hidden
end
 
event :reject do
transition pending: :hidden
end
 
event :publish do
transition [:offline, :pending, :incomplete, :hidden, :published] => :published
end
 
event :hide do
transition [:incomplete, :published, :pending] => :hidden
end
 
event :deprecate do
transition [:incomplete, :published, :offline, :hidden] => :deprecated
end
 
event :mark_as_deleted do
transition [:incomplete, :published, :offline, :hidden] => :deleted, unless: :default_or_last?
end
 
before_transition to: [:deleted], do: :deleted_by_state_machine
after_transition to: [:deleted], do: :notify_deletion
end
 
def accessible?
state != DELETE_STATE
end
 
def using_proxy_pro?
provider_can_use?(:proxy_pro) && proxy.self_managed?
end
 
def publish_events
OIDC::ServiceChangedEvent.create_and_publish!(self)
OIDC::ProxyChangedEvent.create_and_publish!(proxy)
end
 
def backend_authentication_type
if account.try!(:provider_can_use?, :apicast_per_service)
:service_token
else
:provider_key
end
end
 
def backend_authentication_value
case type = backend_authentication_type
when :service_token
service_token
when :provider_key
account.try!(:provider_key)
else
raise "unknown backend_authentication_type: #{type}"
end
end
 
# shouldn't be here .last (?)
# rotation won't work
def active_service_token
service_tokens.first
end
 
def service_token
active_service_token.try(:value)
end
 
def latest_applications
cinstances.latest
end
 
def has_traffic?
cinstances.where.not(first_traffic_at: nil).exists?
end
 
def proxiable?
backend_version.is?(1, 2, :oauth)
end
 
def backend_version
BackendVersion.new(super)
end
 
def published_plans
application_plans.published
end
 
def stop_destroy_if_last_or_default
return if destroyable?
errors.add :base, 'This product cannot be removed'
throw :abort
end
 
# Returns either a service between that service and buyer account or nil.
#
# TODO: test this - used in 'Liquid::PlanWrapper'
#
def service_contract_of(buyer)
service_contracts.find_by(user_account_id: buyer.id)
end
 
# Active service means that it has published service plan
# convenience method
def disabled?
deprecated? || deleted?
end
 
# TODO: Migrate all the customers to use service_id on API calls
# Then remove this notion of default_service
def default?
account.default_service_id == id
end
 
def plans_by_state(state)
application_plans.by_state state
end
 
def provided_by?(account)
self.account == account
end
 
def visible_plans_for(buyer_account)
# TODO: convert this to a scope
results = published_plans.to_a
 
Service#visible_plans_for refers to 'buyer_account' more than self (maybe move it to another class?)
Service#visible_plans_for refers to 'results' more than self (maybe move it to another class?)
Service#visible_plans_for calls 'buyer_account.bought_plan' 2 times
if buyer_account && !results.include?(buyer_account.bought_plan)
results << buyer_account.bought_plan
end
 
results
end
 
# This returns true of false on whether a service has any published plans.
def has_published_plans?
application_plans.exists?("state = 'published' OR live_state = 'published'")
end
 
# Only those metrics that are methods.
def method_metrics
metrics.find_by(system_name: 'hits')&.children || metrics.none
end
 
def top_level_metrics
metrics.top_level
end
 
# Does this service has metric "hits" with submetrics (methods) defined?
def has_method_metrics?
metrics.find_by(system_name: 'hits')&.parent?
end
 
def create_default_metrics
metrics.create_default!(:hits, service_id: id)
end
 
def has_terms?
terms.present?
end
 
# TODO: extract this and Cinstance.search into separate class
# (CinstanceSearcher, or something like that)
def search_cinstances(params)
cinstances.with_account.by_state(params[:state]).search(params)
end
 
def reload(*)
# Kill some cached stuff
@cinstances = nil
super
end
 
Method `to_xml` has 31 lines of code (exceeds 25 allowed). Consider refactoring.
Service#to_xml has approx 26 statements
Method `to_xml` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.
def to_xml(options = {})
xml = options[:builder] || ThreeScale::XML::Builder.new
 
xml.service do |xml|
unless new_record?
xml.id_ id
xml.account_id account_id
end
xml.name name
xml.state state
xml.system_name system_name
xml.backend_version proxy&.authentication_method
xml.description description
 
xml.intentions_required intentions_required
xml.buyers_manage_apps buyers_manage_apps
xml.buyers_manage_keys buyers_manage_keys
xml.referrer_filters_required referrer_filters_required
xml.custom_keys_enabled custom_keys_enabled
xml.buyer_key_regenerate_enabled buyer_key_regenerate_enabled
xml.mandatory_app_key mandatory_app_key
xml.buyer_can_select_plan buyer_can_select_plan
xml.buyer_plan_change_permission buyer_plan_change_permission
annotations_xml(:builder => xml)
 
if notification_settings
xml.notification_settings do |xml|
Service#to_xml contains iterators nested 3 deep
notification_settings.each { |notification, values| xml.tag! notification, values }
end
end
 
xml.deployment_option deployment_option
xml.support_email support_email
 
metrics.to_xml(builder: xml, root: 'metrics')
end
 
xml.to_xml
end
 
# Notification settings cleanup before assign
# calling with nil removes all settings
def notification_settings=(settings)
if settings.present? # actualy some values were passed, so clean them and set them
settings = settings.symbolize_keys
settings.keys.each do |key|
settings[key].map! &:to_i
end
 
self[:notification_settings] = settings
else # nil was passed, so no values were set => clean settings
self[:notification_settings] = {}
end
end
 
def backend_object
@backend_object ||= BackendClient::Connection.new.service(self)
end
 
def notify_alerts?(who, how)
notification_settings.try!(:[], "#{how}_#{who}".to_sym).present?
end
 
def support_email
se = self[:support_email]
if se.present?
se
else
account.try(:support_email)
end
end
 
Method `mode_type` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
def mode_type
return unless proxy
 
if proxy.self_managed?
if oauth?
:oauth
else
:self_managed
end
else
:hosted
end
end
 
def last_accessible?
!provider.accessible_services.where.not(id: id).exists?
end
 
# Backward compatibility with existing providers with default_service_id
def default_or_last?
default? || last_accessible?
end
 
def parameterized_name
name.parameterize
end
 
def parameterized_system_name
system_name.to_s.parameterize.tr('_','-')
end
 
APPLY_I18N = ->(args) do
args.map do |opt|
[
# The `raise:` argument can be removed after upgrading to Rails 7.1, because `I18n.t` should respect the
# `config.i18n.raise_on_missing_translations` config, see https://github.com/rails/rails/commit/6c4f3be929f1f427d6767050848f2fbee8c1f05f
I18n.t(opt, scope: :deployment_options, raise: Rails.application.config.i18n.raise_on_missing_translations),
opt
]
end.to_h.freeze
end
private_constant :APPLY_I18N
 
PLUGINS = APPLY_I18N.call(DeploymentOption.plugins)
private_constant :PLUGINS
 
SERVICE_MESH = APPLY_I18N.call(DeploymentOption.service_mesh)
private_constant :SERVICE_MESH
 
def self.deployment_options(_ = nil)
gateway = APPLY_I18N.call(DeploymentOption.gateways)
 
{ 'Gateway' => gateway, 'Plugin' => PLUGINS, 'Service Mesh' => SERVICE_MESH }
end
 
def deployment_option=(value)
super
ensure
# always set correct proxy endpoints when deployment option changes
(proxy || build_proxy).try(:set_correct_endpoints) if will_save_change_to_deployment_option?
end
 
def backend_version=(backend_version)
(proxy || build_proxy).authentication_method = backend_version
 
if oidc?
super('oauth')
else
super(backend_version)
end
end
 
# This smells of :reek:NilCheck
# But that's OK :)
def oidc_configuration
proxy&.oidc_configuration || OIDCConfiguration.new
end
 
def can_use_policies?
proxy.apicast_configuration_driven && !proxy.service_mesh_integration?
end
 
def can_use_backends?
proxy.apicast_configuration_driven && !proxy.service_mesh_integration?
end
 
# TODO: Remove this when no one use plugins
def plugin_deployment?
DeploymentOption.plugins.include?(deployment_option)
end
 
def create_default_proxy
create_proxy! unless proxy
end
 
private
 
def archive_as_deleted
::DeletedObject.create!(object: self, owner: account)
end
 
def deleted_by_state_machine
@deleted_by_state_machine = true
end
 
def deleted_without_state_machine
if saved_change_to_attribute?(:state) && deleted? && !@deleted_by_state_machine
System::ErrorReporting.report_error('Service has been deleted without using State Machine')
end
end
 
def destroyable?
destroyed_by_association || !default_or_last?
end
 
def create_default_service_plan
service_plans.create!(name: 'Default') { |plan| plan.state = default_service_plan_state }
end
 
Method `default_service_plan_state` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.
def default_service_plan_state
return unless account && account.provider_can_use?(:published_service_plan_signup)
return if account.should_be_deleted?
account.settings.service_plans_ui_visible? ? 'hidden' : 'published'
end
 
def update_notification_settings
current_alert_limits = alert_limits
 
delete_alert_limits(current_alert_limits - notification_settings_levels)
create_alert_limits(notification_settings_levels - current_alert_limits)
end
 
def create_and_publish_service_created_event
Services::ServiceCreatedEvent.create_and_publish!(self, User.current)
end
 
def create_and_publish_service_deleted_event
Services::ServiceDeletedEvent.create_and_publish!(self)
end
 
def notification_settings_levels
(notification_settings || {}).map { |_key, values| values }.flatten.uniq
end
 
def alert_limits
ThreeScale::Core::AlertLimit.load_all(backend_id).map(&:value)
end
 
def delete_alert_limits(*limits)
limits.flatten.each do |limit|
ThreeScale::Core::AlertLimit.delete(backend_id, limit)
end
end
 
def create_alert_limits(*limits)
limits.flatten.each do |limit|
ThreeScale::Core::AlertLimit.save(backend_id, limit)
end
end
 
def update_account_default_service
# cannot use #default? method anymore (ActiveRecord::RecordNotFound)
if account.default_service_id == id && !account.marked_for_destruction?
account.update_columns(default_service_id: nil)
end
end
 
protected
 
# Create an event for scheduled deletion of service
def notify_deletion
Services::ServiceScheduledForDeletionEvent.create_and_publish!(self)
end
 
delegate :provider_id_for_audits, to: :account, allow_nil: true
 
delegate :oauth?, to: :authentication_scheme?
 
delegate :authentication_method, to: :proxy, prefix: true, allow_nil: true
delegate :oidc?, :pending_affecting_changes?, to: :proxy, allow_nil: true
 
def authentication_scheme?
backend_version.to_s.inquiry
end
end