app/models/katello/content_view.rb
module Katello
# rubocop:disable Metrics/ClassLength
class ContentView < Katello::Model
audited :associations => [:repositories, :environments, :filters]
has_associated_audits
include Ext::LabelFromName
include Katello::Authorization::ContentView
include ForemanTasks::Concerns::ActionSubject
include Foreman::ObservableModel
CONTENT_DIR = "content_views".freeze
IMPORT_LIBRARY = "Import-Library".freeze
EXPORT_LIBRARY = "Export-Library".freeze
belongs_to :organization, :inverse_of => :content_views, :class_name => "::Organization"
has_many :content_view_environments, :class_name => "Katello::ContentViewEnvironment", :dependent => :destroy
has_many :environments, :class_name => "Katello::KTEnvironment", :through => :content_view_environments
has_many :content_view_versions, :class_name => "Katello::ContentViewVersion", :dependent => :destroy
alias_method :versions, :content_view_versions
has_one :latest_version_object, -> { latest }, :class_name => "Katello::ContentViewVersion", :dependent => :destroy
# Note the difference between content_view_components and component_composites both refer to
# ContentViewComponent but mean different things.
# content_view_components -> Topdown, given I am a composite CV get the associated components belonging to me
#
# component_composites -> Bottom Up, given I am a component CV get the associated composites that I belong to
#
has_many :content_view_components, :class_name => "Katello::ContentViewComponent", :dependent => :destroy,
:inverse_of => :composite_content_view, :foreign_key => :composite_content_view_id, autosave: true
has_many :component_composites, :class_name => "Katello::ContentViewComponent",
:dependent => :destroy, :inverse_of => :content_view
has_many :content_view_repositories, :class_name => 'Katello::ContentViewRepository',
:dependent => :destroy, :inverse_of => :content_view
has_many :repositories, :through => :content_view_repositories, :class_name => "Katello::Repository",
:after_remove => :remove_repository
has_many :filters, :dependent => :destroy, :class_name => "Katello::ContentViewFilter"
has_many :activation_keys, :class_name => "Katello::ActivationKey", :dependent => :restrict_with_exception
has_many :content_view_environment_content_facets, :class_name => "Katello::ContentViewEnvironmentContentFacet",
:through => :content_view_environments
has_many :content_facets, :class_name => "Katello::Host::ContentFacet", :through => :content_view_environment_content_facets,
:inverse_of => :content_views
has_many :hosts, :class_name => "::Host::Managed", :through => :content_facets,
:inverse_of => :content_views
has_many :hostgroup_content_facets, :class_name => "Katello::Hostgroup::ContentFacet",
:inverse_of => :content_view, :dependent => :nullify
has_many :hostgroups, :class_name => "::Hostgroup", :through => :hostgroup_content_facets,
:inverse_of => :content_view
has_many :repository_references, :class_name => 'Katello::Pulp3::RepositoryReference',
:dependent => :destroy, :inverse_of => :content_view
validates_lengths_from_database :except => [:label]
validates :label, :uniqueness => { :scope => :organization_id },
:presence => true
validates :name, :presence => true, :uniqueness => { :scope => :organization_id }
validates :organization_id, :presence => true
validate :check_non_composite_components
validate :check_docker_conflicts
validate :check_non_composite_auto_publish
validate :check_default_label_name, if: :default?
validates :composite, :inclusion => [true, false]
validates :composite,
inclusion: { in: [false], message: "Composite Content Views can not solve dependencies" },
if: :solve_dependencies
validates :import_only, :inclusion => [true, false]
validates :import_only,
inclusion: { in: [false], message: "Import-only Content Views can not be Composite" },
if: :composite
validates :import_only,
inclusion: { in: [false], message: "Import-only Content Views can not solve dependencies" },
if: :solve_dependencies
validate :import_only_immutable
validates :generated_for,
exclusion: { in: [:none], message: "Generated Content Views can not be Composite" },
if: :composite
validates :generated_for,
exclusion: { in: [:none], message: "Generated Content Views can not solve dependencies" },
if: :solve_dependencies
validates_with Validators::KatelloNameFormatValidator, :attributes => :name
validates_with Validators::KatelloLabelFormatValidator, :attributes => :label
scope :default, -> { where(:default => true) }
scope :non_default, -> { where(:default => false) }
scope :composite, -> { where(:composite => true) }
scope :non_composite, -> { where(:composite => [nil, false]) }
scope :generated, -> { where.not(:generated_for => :none) }
scope :generated_for_repository, -> {
where(:generated_for => [:repository_export,
:repository_import,
:repository_export_syncable])
}
scope :ignore_generated, ->(include_library_generated: false) {
ignored_values = [:repository_export,
:repository_import,
:library_export_syncable,
:repository_export_syncable]
ignored_values += [:library_export, :library_import] if include_library_generated
where.not(generated_for: ignored_values)
}
scope :generated_for_library, -> { where(:generated_for => [:library_export, :library_import, :library_export_syncable]) }
scoped_search :on => :name, :complete_value => true
scoped_search :on => :organization_id, :complete_value => true, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER
scoped_search :on => :label, :complete_value => true
scoped_search :on => :composite, :complete_value => true
scoped_search :on => :generated_for, :complete_value => true
scoped_search :on => :default # just for ordering
scoped_search :on => :name, :complete_value => true,
:rename => :content_views,
:operators => ['='],
:ext_method => :find_components_by_cv_name
def self.find_components_by_cv_name(_key, operator, value)
kcv = Katello::ContentView.table_name
kcvc = Katello::ContentViewComponent.table_name
{ :conditions => "#{kcv}.composite = 't' AND #{kcv}.id IN (SELECT #{kcvc}.composite_content_view_id FROM #{kcvc} WHERE #{kcvc}.content_view_id IN (SELECT #{kcv}.id FROM #{kcv} WHERE #{kcv}.name #{operator} ?))",
:parameter => [value]
}
end
def self.completer_scope_options(search)
if search.include?('content_views')
# Don't autocomplete CCV names when searching for components
{ :value_filter => { :composite => false } }
else
{}
end
end
enum generated_for: {
none: 0,
library_export: 1,
repository_export: 2,
library_import: 3,
repository_import: 4,
library_export_syncable: 5,
repository_export_syncable: 6
}, _prefix: true
set_crud_hooks :content_view
apipie :class, desc: "A class representing #{model_name.human} object" do
name 'Content View'
refs 'ContentView'
sections only: %w[all additional]
prop_group :katello_idname_props, Katello::Model, meta: { friendly_name: 'Content View' }
property :label, String, desc: 'Returns label of the Content View'
property :organization, 'Organization', desc: 'Returns organization object'
end
def self.in_environment(env)
joins(:content_view_environments).
where("#{Katello::ContentViewEnvironment.table_name}.environment_id = ?", env.id)
end
def self.published_with_repositories(root_repository)
joins(:content_view_versions => :repositories).where("katello_repositories.root_id" => root_repository.id).uniq
end
def self.in_organization(org)
where(organization_id: org.id) unless org.nil?
end
def to_s
name
end
def library_import?
name == IMPORT_LIBRARY
end
def library_export?
name.start_with? EXPORT_LIBRARY
end
def generated_for_repository?
generated_for_repository_export? || generated_for_repository_import? || generated_for_repository_export_syncable?
end
def generated_for_library?
generated_for_library_export? || generated_for_library_import? || generated_for_library_export_syncable?
end
def content_host_count
hosts.size
end
def component_ids
components.map(&:id)
end
def components
content_view_components.map(&:latest_version).compact.freeze
end
def sorted_versions
versions.order('created_at DESC')
end
# Adds content view components based on the input
# [{:content_view_version_id=>1, :latest=> false}, {:content_view_id=>1, :latest=> true} ..]
def add_components(components_to_add)
components_to_add.each do |cvc|
content_view_components.build(cvc)
end
end
# Removes selected content view components
# [1,2,34] => content view component ids/
def remove_components(components_to_remove)
content_view_components.where(:id => components_to_remove).destroy_all
end
# Warning this call wipes out existing associations
# And replaces them with the component version ids passed in.
def component_ids=(component_version_ids_to_set)
content_view_components.destroy_all
component_version_ids_to_set.each do |content_view_version_id|
cvv = ContentViewVersion.find(content_view_version_id)
content_view_components.build(:content_view_version => cvv,
:latest => false,
:composite_content_view => self)
end
end
def copy_components(new_view)
self.content_view_components.each do |cvc|
component = cvc.dup
component.composite_content_view = new_view
new_view.content_view_components << component
end
end
def copy_filters(new_view)
self.filters.each do |filter|
new_filter = filter.dup
new_filter.repositories = filter.repositories
new_view.filters << new_filter
case filter.type
when ContentViewDebFilter.name
filter.deb_rules.each do |rule|
new_filter.deb_rules << rule.dup
end
when ContentViewPackageFilter.name
filter.package_rules.each do |rule|
new_filter.package_rules << rule.dup
end
when ContentViewPackageGroupFilter.name
filter.package_group_rules.each do |rule|
new_filter.package_group_rules << rule.dup
end
when ContentViewErratumFilter.name
filter.erratum_rules.each do |rule|
new_filter.erratum_rules << rule.dup
end
end
end
end
def copy(new_name)
new_view = ContentView.new
new_view.name = new_name
new_view.attributes = self.attributes.slice("description", "organization_id", "default", "composite", "solve_dependencies")
new_view.save!
new_view.repositories = self.repositories
copy_components(new_view)
copy_filters(new_view)
new_view.save!
new_view
end
def promoted?
# if the view exists in more than 1 environment, it has been promoted
self.environments.many?
end
def generated?
!generated_for_none?
end
#NOTE: this function will most likely become obsolete once we drop api v1
def as_json(options = {})
result = self.attributes
result['organization'] = self.organization.try(:name)
result['environments'] = environments.map { |e| e.try(:name) }
result['versions'] = versions.map(&:version)
result['versions_details'] = versions.map do |v|
{
:version => v.version,
:published => v.created_at.to_s,
:environments => v.environments.map { |e| e.name }
}
end
if options && options[:environment].present?
result['repositories'] = repos(options[:environment]).map(&:name)
end
result
end
def total_package_count(env)
Katello::Rpm.in_repositories(self.repos(env)).count
end
def total_deb_package_count(env)
Katello::Deb.in_repositories(self.repos(env)).count
end
def in_environment?(env)
environments.include?(env)
end
apipie :method, 'Returns the Katello::ContentViewVersion for a given Lifecycle Environment' do
required :env, 'Katello::KTEnvironment', desc: 'a __Katello::KTEnvironment__ object for which we load the __Katello::ContentViewVersion__ object'
returns 'Katello::ContentViewVersion'
end
def version(env)
self.versions.in_environment(env).order("#{Katello::ContentViewVersion.table_name}.id ASC").readonly(false).last
end
def latest_version
latest_version_object.try(:version)
end
def latest_version_id
latest_version_object.try(:id)
end
def latest_version_env
latest_version_object.try(:environments) || []
end
def last_task
last_task_id = history.order(:created_at)&.last&.task_id
last_task_id ? ForemanTasks::Task.find_by(id: last_task_id) : nil
end
def history
Katello::ContentViewHistory.joins(:content_view_version).where(
"#{Katello::ContentViewVersion.table_name}.content_view_id" => self.id)
end
def version_environment(env)
# TODO: rewrite this into SQL or use content_view_environment when that
# points to environment
version(env).content_view_version_environments.select { |cvve| cvve.environment_id == env.id }
end
def resulting_products
(self.repositories.collect { |r| r.product }).uniq
end
def repos(env = nil)
if env
repo_ids = versions.flat_map { |version| version.repositories.in_environment(env) }.map(&:id)
else
repo_ids = versions.flat_map { |version| version.repositories }.map(&:id)
end
Repository.where(:id => repo_ids)
end
def library_repos
Repository.where(:id => library_repo_ids)
end
def library_repo_ids
repos(self.organization.library).map { |r| r.library_instance_id }
end
def all_version_repos
Repository.joins(:content_view_version).
where("#{Katello::ContentViewVersion.table_name}.content_view_id" => self.id)
end
def repositories_to_publish(override_components = nil)
if composite?
components_to_publish = []
components.each do |component|
override_component = override_components&.detect do |override_cvv|
override_cvv.content_view == component.content_view
end
if override_component
components_to_publish << override_component
else
components_to_publish << component
end
end
ids = components_to_publish.flat_map { |version| version.repositories.archived }.map(&:id)
Repository.where(:id => ids)
else
repositories
end
end
def repositories_to_publish_ids
composite? ? repositories_to_publish.pluck(&:id) : repository_ids
end
def repositories_to_publish_by_library_instance(override_components = nil)
# retrieve the list of repositories in a hash, where the key
# is the library instance id, and the value is an array
# of the repositories for that instance.
repositories_to_publish(override_components).inject({}) do |result, repo|
result[repo.library_instance] ||= []
result[repo.library_instance] << repo
result
end
end
def duplicate_repositories_to_publish
return [] unless composite?
repositories_to_publish_by_library_instance.select { |key, val| val.count > 1 && key.present? }.keys
end
def components_with_repo(library_instance)
components.select { |component| component.repositories.where(:library_instance => library_instance).any? }
end
def auto_publish_components
component_composites.where(latest: true).joins(:composite_content_view).where(self.class.table_name => { auto_publish: true })
end
def publish_repositories(override_components = nil)
repositories = composite? ? repositories_to_publish_by_library_instance(override_components).values : repositories_to_publish
repositories.each do |repos|
if repos.is_a? Array
yield repos
else
yield [repos]
end
end
end
def update_host_statuses(environment)
# update errata applicability counts for all hosts in the CV & LE
Location.no_taxonomy_scope do
User.as_anonymous_admin do
::Katello::Host::ContentFacet.in_content_views_and_environments(
content_views: [self],
lifecycle_environments: [environment]
).each do |facet|
facet.update_applicability_counts
facet.update_errata_status
end
end
end
end
def component_repositories
components.map(&:archived_repos).flatten
end
def component_repository_ids
component_repositories.map(&:id)
end
def repos_in_product(env, product)
version = version(env)
if version
version.repositories.in_environment(env).in_product(product)
else
[]
end
end
def products(env = nil)
repos = repos(env)
Product.joins(:repositories).where("#{Katello::Repository.table_name}.id" => repos.map(&:id)).distinct
end
#get the library instances of all repos within this view
def all_version_library_instances
all_repos = all_version_repos.where(:library_instance_id => nil).pluck("#{Katello::Repository.table_name}.id")
all_repos += all_version_repos.pluck(:library_instance_id)
Repository.where(:id => all_repos)
end
def get_repo_clone(env, repo)
lib_id = repo.library_instance_id || repo.id
Repository.in_environment(env).where(:library_instance_id => lib_id).
joins(:content_view_version).
where("#{Katello::ContentViewVersion.table_name}.content_view_id" => self.id)
end
def delete(from_env)
if from_env.library? && in_non_library_environment?
fail Errors::ChangesetContentException, _("Cannot delete view while it exists in environments")
end
version = self.version(from_env)
if version.nil?
fail Errors::ChangesetContentException, _("Cannot delete from %s, view does not exist there.") % from_env.name
end
version = ContentViewVersion.find(version.id)
if (foreman_env = Environment.find_by_katello_id(self.organization, from_env, self))
foreman_env.destroy
end
version.delete(from_env)
self.destroy if self.versions.empty?
end
def in_non_library_environment?
environments.where(:library => false).length > 0
end
def duplicate_repositories
counts = repositories_to_publish.each_with_object(Hash.new(0)) do |repo, h|
h[repo.library_instance_id] += 1
end
ids = counts.select { |_k, v| v > 1 }.keys
Repository.where(:id => ids)
end
def duplicate_docker_repos
duplicate_repositories.docker_type
end
def check_non_composite_components
if !composite? && components.present?
errors.add(:base, _("Cannot add component versions to a non-composite content view"))
end
end
def check_non_composite_auto_publish
if !composite? && auto_publish
errors.add(:base, _("Cannot set auto publish to a non-composite content view"))
end
end
def check_default_label_name
if default? && !(name == 'Default Organization View' && label == 'Default_Organization_View')
errors.add(:base, _("Name and label of default content view should not be changed"))
end
end
def check_docker_conflicts
duplicate_docker_repos.each do |repo|
msg = _("Container Image repo '%{repo}' is present in multiple component content views.") % { repo: repo.name }
errors.add(:base, msg)
end
end
def content_view_environment(environment)
self.content_view_environments.where(:environment_id => environment.try(:id)).first
end
def update_cp_content(env)
view_env = content_view_environment(env)
view_env&.update_cp_content
end
# Associate an environment with this content view. This can occur whenever
# a version of the view is promoted to an environment. It is necessary for
# candlepin to become aware that the view is available for consumers.
def add_environment(env, version)
if self.content_view_environments.where(:environment_id => env.id).empty?
label = generate_cp_environment_label(env)
ContentViewEnvironment.create!(:name => label,
:label => label,
:cp_id => generate_cp_environment_id(env),
:environment_id => env.id,
:content_view => self,
:content_view_version => version
)
end
end
# Unassociate an environment from this content view. This can occur whenever
# a view is deleted from an environment. It is necessary to make candlepin
# aware that the view is no longer available for consumers.
def remove_environment(env)
# Do not remove the content view environment, if there is still a view
# version in the environment.
if self.versions.in_environment(env).blank?
view_env = self.content_view_environments.where(:environment_id => env.id)
view_env.first.destroy unless view_env.blank?
end
end
def cp_environment_label(env)
ContentViewEnvironment.where(:content_view_id => self, :environment_id => env).first.try(:label)
end
def cp_environment_id(env)
ContentViewEnvironment.where(:content_view_id => self, :environment_id => env).first.try(:cp_id)
end
def create_new_version(major = next_version, minor = 0, components = self.components)
version = ContentViewVersion.create!(:major => major,
:minor => minor,
:content_view => self,
:components => components
)
# TODO: If a controller creates a new version and then uses latest_version_object, the old data is displayed.
# To prevent this, a 'reload' would currently be necessary, but this is not very performant.
# However, this is currently not a problem because after your create_new_version there is no immediate
# access to latest_version_object, but the ContentView object is first completely reloaded.
#
# In Rails 7.1, individual connections can be reloaded:
# https://www.shakacode.com/blog/rails-7-1-allows-resetting-singular-associations/
update(:next_version => major.to_i + 1) unless major.to_i < next_version
version
end
def check_ready_to_import!
fail _("Cannot import a composite content view") if composite?
fail _("This Content View must be set to Import-only before performing an import") unless import_only?
true
end
def check_ready_to_publish!(importing: false, syncable: false)
fail _("User must be logged in.") if ::User.current.nil?
fail _("Cannot publish default content view") if default?
if importing
check_ready_to_import!
else
fail _("Import-only content views can not be published directly") if import_only? && !syncable
check_repositories_blocking_publish!
check_composite_action_allowed!(organization.library)
check_docker_repository_names!([organization.library])
check_orphaned_content_facets!(environments: self.environments)
end
true
end
def check_repositories_blocking_publish!
blocking_tasks = repositories&.map { |repo| repo.blocking_task }&.compact
if blocking_tasks&.any?
errored_tasks = blocking_tasks.uniq.map { |task| "- #{Setting['foreman_url']}/foreman_tasks/tasks/#{task&.id}" }.join("\n")
fail _("Pending tasks detected in repositories of this content view. Please wait for the tasks: " +
errored_tasks + " before publishing.")
end
end
def check_docker_repository_names!(environments)
environments.each do |environment|
repositories = []
publish_repositories do |all_repositories|
repositories += all_repositories.keep_if { |repository| repository.content_type == Katello::Repository::DOCKER_TYPE }
end
next if repositories.empty?
error_messages = ::Katello::Validators::EnvironmentDockerRepositoriesValidator.validate_repositories(environment.registry_name_pattern, repositories)
unless error_messages.empty?
error_messages << _("Consider changing the Lifecycle Environment's Registry Name Pattern to something more specific.")
fail error_messages.join(" ")
end
end
true
end
def check_composite_action_allowed!(env)
if composite? && Setting['restrict_composite_view']
if components.size != content_view_components.size
fail _("Make sure all the component content views are published before publishing/promoting the composite content view. "\
"This restriction is optional and can be modified in the Administrator -> Settings -> Content "\
"page using the restrict_composite_view flag.")
end
env_ids = env.try(:pluck, 'id') || []
env_ids << env.id unless env_ids.size > 0
components.each do |component|
component_environment_ids = component.environments.pluck('id')
unless (env_ids - component_environment_ids).empty?
fail _("The action requested on this composite view cannot be performed until all of the "\
"component content view versions have been promoted to the target environment: %{env}. "\
"This restriction is optional and can be modified in the Administrator -> Settings -> Content "\
"page using the restrict_composite_view flag.") %
{ :env => env.try(:pluck, 'name') || env.name }
end
end
end
true
end
def check_orphaned_content_facets!(environments: [])
Location.no_taxonomy_scope do
User.as_anonymous_admin do
::Katello::Host::ContentFacet.in_content_views_and_environments(
content_views: [self],
lifecycle_environments: environments
).each do |facet|
unless facet.host
fail _("Orphaned content facets for deleted hosts exist for the content view and environment. Please run rake task : katello:clean_orphaned_facets and try again!")
end
end
end
end
end
def check_remove_from_environment!(env)
errors = []
dependencies = { hosts: _("hosts"),
activation_keys: _("activation keys")
}
dependencies.each do |key, name|
if (models = self.association(key).scope.in_environment(env)).any?
errors << _("Cannot remove '%{view}' from environment '%{env}' due to associated %{dependent}: %{names}.") %
{ view: self.name, env: env.name, dependent: name, names: models.map(&:name).join(", ") }
end
end
fail errors.join(" ") if errors.any?
return true
end
def check_ready_to_destroy!
errors = []
dependencies = { environments: _("environments"),
hosts: _("hosts"),
activation_keys: _("activation keys")
}
dependencies.each do |key, name|
if (models = self.association(key).scope).any?
errors << _("Cannot delete '%{view}' due to associated %{dependent}: %{names}.") %
{ view: self.name, dependent: name, names: models.map(&:name).join(", ") }
end
end
fail errors.join(" ") if errors.any?
return true
end
def self.humanize_class_name(_name = nil)
_("Content Views")
end
def version_count
content_view_versions.size
end
def on_demand_repositories
repositories.on_demand
end
def related_cv_count
if composite
content_view_components.length
else
component_composites.length
end
end
def related_composite_cvs
content_views = []
component_composites.each do |cv|
cv_id = cv.composite_content_view_id
cv_name = ContentView.find(cv_id).name
content_views.push(
{
id: cv_id,
name: cv_name
}
)
end
content_views
end
def composite_cv_components_changed?
return true unless latest_version_object
published_component_version_ids = latest_version_object.components.pluck(:id) || []
unpublished_component_version_ids = content_view_components.where(latest: false).pluck(:content_view_version_id) || []
content_view_components.where(latest: true).each do |latest_component|
unpublished_component_version_ids << latest_component.content_view&.latest_version_object&.id
end
published_component_version_ids.compact.uniq.sort != unpublished_component_version_ids.compact.uniq.sort
end
def last_publish_task_success?
last_publish_result = latest_version_object&.history&.publish&.first&.task&.result
return last_publish_result.present? && last_publish_result == 'success'
end
def cv_repo_indexed_after_last_published?
repositories.any? { |repo| repo.last_indexed && repo.last_indexed > latest_version_object.created_at }
end
def unpublishable?
default? || import_only? || generated?
end
def needs_publish?
#Returns
# True:
# a) When content/repo/filter change audit records exist
# b) CV hasn't ever been published
# c) CV dependency_solving != latest_version.applied_filters.dependency_solving
# d) If repo was indexed after cv publish. This can happen under 3 cases:
# i) Index runs because last index(before publish) had failed and repo is picked up for index even if pulp publication hasn't changed.
# ii) Complete sync runs or sync adds/removes new content (Already true because new pulp publication/version gets created)
# iii) repo.index_content is run. (This doesn't necessarily indicate contents changed. Corner case where we play safe and return true)
# nil:
# a) When CV version creation audit is missing(Indicating audit cleanup)
# b) Version doesn't have audited_filters set indicating
# it was published before 4.9 upgrade when we started auditing changes on the CV.
# c) Last publish task failed leaving us with no way of knowing if all content in the version is correct.
# False:
# a) No changes were detected via audits *and*
# Audit for CV publish exists (Audits haven't been cleaned up)
# *and* applied_filters field is set(Published after upgrade)
# b) Default, import only and generated CVs can not be published, hence these will always return false.
#
return false if unpublishable?
return true unless latest_version_object
return nil unless last_publish_task_success?
return composite_cv_components_changed? if composite?
# return true if the audit records clearly show we have unpublished changes
return true if audited_changes_present?
# return true if the dependency solving changed for CV between last publish and now
return true if dependency_solving_changed?
# return true if any child repo's indexed_at > last_version.created_at
return true if cv_repo_indexed_after_last_published?
# if we didn't return `true` already, either the audit records show that we don't need to publish, or we may
# have insufficient data to make the determination (either audits were cleaned, or never got created at all).
# first, check for the `create` audit record; its absence indicates that audits were cleaned some time after
# the cv version was created (i.e. the first indeterminate state) so we return `nil` in that case.
return nil unless latest_version_object&.audits&.where(action: "create")&.exists?
# even when the `create` audit exists, the other audits could still be absent due to the latest cv version
# being created prior to the tracking of the other audits that were added in katello 4.9 (i.e. the second indeterminate state).
# We determine that using the `applied_filters` field. This field was added in Katello 4.9 and is set to nil for
# all versions published before that upgrade.
# If `applied_filters` is nil we can not deterministically rule out changes before the upgrade
# not captured by newer content change and filter change audits.
# If that field is not nil, the version was published after upgrade, hence we have all the information to rule out
# any audited changes to the CV and we can deterministically return false
latest_version_object.applied_filters.nil? ? nil : false
end
def audited_changes_present?
latest_version_created_at = latest_version_object.created_at
cv_repository_ids = repositories.pluck(:id)
audited_changes_like = ->(param) {
Arel.sql("#{Audit.table_name}.audited_changes ilike '%#{param}%'")
}
table = Audit.arel_table
repository_condition = table[:auditable_id].eq(id).and(audited_changes_like.call("repository_ids"))
cv_repository_condition = table[:auditable_id].in(cv_repository_ids)
.and(table[:auditable_type].eq('Katello::Repository'))
.and(Arel.sql("(#{audited_changes_like.call("publication_href")} OR #{audited_changes_like.call("version_href")})"))
content_view_filter_condition = table[:auditable_type].eq('Katello::ContentViewFilter').and(table[:associated_id].eq(id))
filter_rule_condition = table[:associated_id].eq(id).and(table[:auditable_type].matches('%FilterRule%'))
base_query = table[:created_at].gt(latest_version_created_at)
final_query = base_query.and(repository_condition.or(cv_repository_condition).or(content_view_filter_condition).or(filter_rule_condition))
Audit.where(final_query).exists?
end
def dependency_solving_changed?
latest_version_object.applied_filters && solve_dependencies != latest_version_object.applied_filters['dependency_solving']
end
def filtered?
filters.present?
end
def blocking_task
blocking_task_labels = [
::Actions::Katello::ContentView::Publish.name
]
ForemanTasks::Task::DynflowTask.where(:label => blocking_task_labels)
.where.not(state: 'stopped')
.for_resource(self)
.order(:started_at)
.last
end
protected
def remove_repository(repository)
filters.each do |filter_item|
repo_exists = Repository.unscoped.joins(:filters).where(
ContentViewFilter.table_name => { :id => filter_item.id }, :id => repository.id).count
if repo_exists
filter_item.repositories.delete(repository)
filter_item.save!
end
end
end
private
def import_only_immutable
if import_only_changed? && self.persisted?
errors.add(:import_only, _("Import-only can not be changed after creation"))
end
end
def generate_cp_environment_label(env)
# The label for a default view, will simply be the env label; otherwise, it
# will be a combination of env and view label. The reason being, the label
# for a default view is internally generated (e.g. 'Default_View_for_dev')
# and we do not need to expose it to the user.
self.default ? env.label : [env.label, self.label].join('/')
end
def generate_cp_environment_id(env)
# The id for a default view, will simply be the org label; otherwise, it
# will be a combination of env id and view id. The reason being,
# for a default view, the same candlepin environment will be referenced
# by the kt_environment and content_view_environment.
value = self.default ? env.organization.label.to_s : [env.organization.label, env.label, self.label].join('-')
Katello::Util::Data.hexdigest(value)
end
def confirm_not_promoted
if promoted?
errors.add(:base, _("cannot be deleted if it has been promoted."))
return false
end
return true
end
def related_resources
self.organization
end
class Jail < ::Safemode::Jail
allow :id, :name, :label, :version, :organization
end
end
end