Katello/katello

View on GitHub
app/models/katello/content_view_version.rb

Summary

Maintainability
D
1 day
Test Coverage
module Katello
  # rubocop:disable Metrics/ClassLength
  class ContentViewVersion < Katello::Model
    include Authorization::ContentViewVersion
    include ForemanTasks::Concerns::ActionSubject
    include Katello::Concerns::SearchByRepositoryName

    define_model_callbacks :promote, :only => [:before, :after]
    audited :associations => [:repositories, :environments]
    before_destroy :validate_destroyable!

    belongs_to :content_view, :class_name => "Katello::ContentView", :inverse_of => :content_view_versions
    has_many :content_view_environments, :class_name => "Katello::ContentViewEnvironment",
                                         :dependent => :destroy
    has_many :environments, :through      => :content_view_environments,
                            :class_name   => "Katello::KTEnvironment",
                            :inverse_of   => :content_view_versions,
                            :after_remove => :remove_environment

    has_many :history, :class_name => "Katello::ContentViewHistory", :inverse_of => :content_view_version,
                       :dependent => :destroy, :foreign_key => :katello_content_view_version_id

    has_many :triggered_histories, :class_name => "Katello::ContentViewHistory", :dependent => :destroy,
             :inverse_of => :triggered_by, :foreign_key => :triggered_by_id

    has_many :export_histories, :class_name => "::Katello::ContentViewVersionExportHistory", :dependent => :destroy,
             :inverse_of => :content_view_version
    has_many :import_histories, :class_name => "::Katello::ContentViewVersionImportHistory", :dependent => :destroy,
             :inverse_of => :content_view_version
    has_many :repositories, :class_name => "::Katello::Repository", :dependent => :destroy
    has_one :task_status, :class_name => "Katello::TaskStatus", :as => :task_owner, :dependent => :destroy

    has_many :content_view_components, :class_name => "Katello::ContentViewComponent",
             :inverse_of => :content_view_version, :dependent => :destroy
    has_many :composite_content_views, :through => :content_view_components, :source => :composite_content_view

    has_many :content_view_version_components, :inverse_of => :composite_version, :dependent => :destroy, :foreign_key => :composite_version_id,
             :class_name => "Katello::ContentViewVersionComponent"
    has_many :components, :through => :content_view_version_components, :source => :component_version,
             :class_name => "Katello::ContentViewVersion", :inverse_of => :composites

    has_many :content_view_version_composites, :inverse_of => :component_version, :dependent => :destroy, :foreign_key => :component_version_id,
             :class_name => "Katello::ContentViewVersionComponent"
    has_many :composites, :through => :content_view_version_composites, :source => :composite_version,
             :class_name => "Katello::ContentViewVersion", :inverse_of => :components
    has_many :published_in_composite_content_views, through: :composites, source: :content_view
    delegate :default, :default?, to: :content_view

    validates_lengths_from_database

    validates :minor, :uniqueness => {:scope => [:content_view_id, :major], :message => N_(", must be unique to major and version id version.")}
    validates :minor, numericality: true

    scope :default_view, -> { joins(:content_view).where("#{Katello::ContentView.table_name}.default" => true) }
    scope :non_default_view, -> { joins(:content_view).where("#{Katello::ContentView.table_name}.default" => false) }
    scope :with_organization_id, ->(organization_id) do
      joins(:content_view).where("#{Katello::ContentView.table_name}.organization_id" => organization_id)
    end
    scope :not_ignorable, -> { where(content_view_id: Katello::ContentView.ignore_generated) }
    scope :triggered_by, ->(content_view_version_id) do
      sql = Katello::ContentViewHistory.where(:triggered_by_id => content_view_version_id).select(:katello_content_view_version_id).to_sql
      where("#{Katello::ContentViewVersion.table_name}.id in (#{sql})")
    end

    scope :with_repositories, ->(repositories) do
      joins(:repositories).includes(:content_view).merge(repositories).distinct
    end

    scope :latest, -> { order('major DESC', 'minor DESC').limit(1) }

    scoped_search :on => :content_view_id, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER
    scoped_search :on => :major, :rename => :version, :complete_value => true, :ext_method => :find_by_version
    serialize :content_counts

    def self.find_by_version(_key, operator, value)
      conditions = ""
      if ['>', '<', '=', '<=', '>=', "<>", "!=", 'IN', 'NOT IN'].include?(operator) && value.to_f >= 0
        major, minor = value.split(".")
        case
        when /[<>]/ =~ operator
          minor ||= 0
          query = where("major #{operator} :major OR (major = :major AND minor #{operator} :minor)", :major => major, :minor => minor)
        when minor.nil?
          query = where("major #{operator} (:major)", :major => major)
        else
          query = where("major #{operator} (:major) and minor #{operator} (:minor)", :major => major, :minor => minor)
        end
        _, conditions = query.to_sql.split("WHERE")
      end
      { :conditions => conditions }
    end

    def self.component_of(versions)
      joins(:content_view_version_composites).where("#{Katello::ContentViewVersionComponent.table_name}.composite_version_id" => versions)
    end

    def self.with_library_repo(repo)
      joins(:repositories).where("#{Katello::Repository.table_name}.library_instance_id" => repo)
    end

    def self.for_version(version)
      major, minor = version.to_s.split('.')
      minor ||= 0
      where(:major => major, :minor => minor)
    end

    def to_s
      name
    end

    def self.contains_file(file_unit_id)
      where(id: Katello::Repository.where(id: Katello::RepositoryFileUnit.where(file_unit_id: file_unit_id).select(:repository_id)).select(:content_view_version_id))
    end

    def ansible_collections
      AnsibleCollection.in_repositories(archived_repos)
    end

    delegate :organization, to: :content_view

    def active_history
      self.history.select { |history| history.task.try(:pending) }
    end

    def last_event
      self.history.order(:created_at).last
    end

    def env_promote_date(env)
      Katello::ContentViewEnvironment.where(:environment_id => env.id, :content_view_version_id => self.id).pluck(:updated_at).try(:first)
    end

    def name
      "#{content_view} #{version}"
    end

    def description
      history.publish.first.try(:notes)
    end

    def default_content_view?
      default?
    end

    def latest?
      content_view.latest_version_id == self.id
    end

    def in_composite?
      composite_content_views.any?
    end

    def published_in_composite?
      content_view_version_composites.any?
    end

    def in_environment?
      environments.any?
    end

    def available_releases
      Katello::RootRepository.where(:id => self.repositories.select(:root_id)).pluck(:minor).compact.uniq.sort
    end

    def next_incremental_version
      "#{major}.#{minor + 1}"
    end

    def version
      "#{major}.#{minor}"
    end

    def incrementally_updated?
      minor != 0
    end

    def repos(env)
      self.repositories.in_environment(env)
    end

    def archived_repos
      self.default? ? self.repositories : self.repos(nil)
    end

    def non_archive_repos
      self.repositories.non_archived
    end

    def library_repos
      archived_repos.includes(:library_instance).map(&:library_instance)
    end

    def products(env = nil)
      if env
        repos(env).map(&:product).uniq(&:id)
      else
        self.repositories.map(&:product).uniq(&:id)
      end
    end

    def repos_ordered_by_product(env)
      # The repository model has a default scope that orders repositories by name;
      # however, for content views, it is desirable to order the repositories
      # based on the name of the product the repository is part of.
      Repository.send(:with_exclusive_scope) do
        self.repositories.joins(:product).in_environment(env).order("#{Katello::Product.table_name}.name asc")
      end
    end

    def get_repo_clone(env, repo)
      lib_id = repo.library_instance_id || repo.id
      self.repos(env).where("#{Katello::Repository.table_name}.library_instance_id" => lib_id)
    end

    def self.in_environment(env)
      joins(:content_view_environments).where("#{Katello::ContentViewEnvironment.table_name}.environment_id" => env)
    end

    def removable?
      if environments.blank?
        content_view.promotable_or_removable?
      else
        content_view.promotable_or_removable? && KTEnvironment.where(:id => environments).any_promotable?
      end
    end

    def deletable?(from_env)
      ::Host.in_content_view_environment(self.content_view, from_env).empty? ||
          self.content_view.versions.in_environment(from_env).count > 1
    end

    def promotable?(target_envs)
      target_envs = Array.wrap(target_envs)
      all_environments = target_envs + environments
      target_envs.all? do |environment|
        all_environments.include?(environment.prior) || environments.empty? && environment == organization.library
      end
    end

    def components_needing_errata(errata)
      component_repos = Repository.where(:content_view_version_id => self.components)
      library_repos = Repository.where(:id => component_repos.pluck(:library_instance_id)).with_errata(errata)
      component_repos -= component_repos.with_errata(errata) #find component repos without the errata
      component_repos.select { |repo| library_repos.include?(repo.library_instance) }.map(&:content_view_version).uniq
    end

    def packages
      Rpm.in_repositories(archived_repos)
    end

    def generic_content_units(content_type)
      GenericContentUnit.in_repositories(archived_repos).where(content_type: content_type)
    end

    def library_packages
      Rpm.in_repositories(library_repos)
    end

    def available_packages
      # The simple/obvious solution is:
      #   library_packages.where.not(:id => packages)
      # However, when the list of exclusions is large, the SQL "NOT IN" clause
      # is extremely inefficient, and it is much better to use a
      # "LEFT OUTER JOIN" with a subquery.
      # ActiveRecord .joins() only supports subqueries by supplying raw SQL.  We
      # use .to_sql to avoid hard-coding raw SQL for self.packages, although
      # .to_sql may also be somewhat brittle.  For example, see:
      # https://github.com/rails/rails/issues/18379
      library_packages.joins(
        "LEFT OUTER JOIN (#{packages.select('id').to_sql}) AS exclude_rpms ON " \
        'katello_rpms.id = exclude_rpms.id'
      ).where('exclude_rpms.id IS NULL')
    end

    def srpms
      Katello::Srpm.in_repositories(self.repositories)
    end

    def module_streams
      ModuleStream.in_repositories(archived_repos)
    end

    def docker_tags
      # Don't count tags from non-archived repos; this causes count errors
      ::Katello::DockerMetaTag.where(:id => RepositoryDockerMetaTag.where(:repository_id => repositories.archived.docker_type).select(:docker_meta_tag_id))
    end

    def debs
      Katello::Deb.in_repositories(self.repositories.archived)
    end

    def library_debs
      Katello::Deb.in_repositories(library_repos)
    end

    def available_debs
      library_packages.where.not(:id => debs)
    end

    def errata(errata_type = nil)
      errata = Erratum.in_repositories(archived_repos)
      errata = errata.of_type(errata_type) if errata_type
      errata
    end

    def library_errata
      Erratum.in_repositories(library_repos)
    end

    def available_errata
      # The simple/obvious solution is:
      #   library_errata.where.not(:id => errata)
      # However, when the list of exclusions is large, the SQL "NOT IN" clause
      # is extremely inefficient, and it is much better to use a
      # "LEFT OUTER JOIN" with a subquery.
      # ActiveRecord .joins() only supports subqueries by supplying raw SQL.  We
      # use .to_sql to avoid hard-coding raw SQL for self.errata, although
      # .to_sql may also be somewhat brittle.  For example, see:
      # https://github.com/rails/rails/issues/18379
      library_errata.joins(
        "LEFT OUTER JOIN (#{errata.select('id').to_sql}) AS exclude_errata ON " \
        'katello_errata.id = exclude_errata.id'
      ).where('exclude_errata.id IS NULL')
    end

    def file_units
      FileUnit.in_repositories(archived_repos)
    end

    def docker_manifests
      DockerManifest.in_repositories(archived_repos)
    end

    def docker_manifest_lists
      DockerManifestList.in_repositories(archived_repos)
    end

    def package_groups
      PackageGroup.in_repositories(archived_repos)
    end

    def update_content_counts!
      self.content_counts = {}
      RepositoryTypeManager.indexable_content_types.each do |content_type|
        case content_type&.model_class::CONTENT_TYPE
        when DockerTag::CONTENT_TYPE
          content_counts[DockerTag::CONTENT_TYPE] = docker_tags.count
        when GenericContentUnit::CONTENT_TYPE
          content_counts[content_type.content_type] = content_type&.model_class&.in_repositories(self.repositories.archived)&.where(:content_type => content_type.content_type)&.count
        else
          content_counts[content_type&.model_class::CONTENT_TYPE] = content_type&.model_class&.in_repositories(self.repositories.archived)&.count
        end
      end
      save!
    end

    def auto_publish_composites!
      metadata = {
        description: _("Auto Publish - Triggered by '%s'") % self.name,
        triggered_by: self.id
      }
      self.content_view.auto_publish_components.pluck(:composite_content_view_id).each do |composite_id|
        ::Katello::EventQueue.push_event(::Katello::Events::AutoPublishCompositeView::EVENT_TYPE, composite_id) do |attrs|
          attrs[:metadata] = metadata
        end
      end
    end

    def repository_type_counts_map
      counts = {}
      Katello::RepositoryTypeManager.enabled_repository_types.keys.each do |repo_type|
        counts["#{repo_type}_repository_count"] = archived_repos.with_type(repo_type).count
      end
      counts
    end

    def content_counts_map
      # if its empty, calculate it on demand
      update_content_counts! if content_counts.blank?
      counts = Hash[content_counts.map { |key, value| ["#{key}_count", value] }]
      counts.merge("module_stream_count" => counts["modulemd_count"],
                   "package_count" => counts["rpm_count"])
    end

    def check_ready_to_promote!(to_env)
      fail _("Default content view versions cannot be promoted") if default?
      content_view.check_composite_action_allowed!(to_env)
      content_view.check_docker_repository_names!(to_env)
      content_view.check_orphaned_content_facets!(environments: [to_env])
    end

    def validate_destroyable!(skip_environment_check: false)
      unless organization.being_deleted?
        if !skip_environment_check && in_environment?
          fail _("Cannot delete version while it is in environments: %s") %
                   environments.map(&:name).join(",")
        end

        if in_composite?
          fail _("Cannot delete version while it is in use by composite content views: %s") %
                   composite_content_views.map(&:name).join(",")
        end

        if published_in_composite?
          list = composites.map do |version|
            "#{version.content_view.name} Version #{version.version}"
          end
          fail _("Cannot delete version while it is in use by composite content views: %s") %
                   list.join(",")
        end
      end
      true
    end

    def importable_repositories
      if default?
        repositories.exportable
      else
        archived_repos.exportable
      end
    end

    def before_promote_hooks
      run_callbacks :sync do
        logger.debug "custom hook before_promote on #{name} will be executed if defined."
        true
      end
    end

    def after_promote_hooks
      run_callbacks :sync do
        logger.debug "custom hook after_promote on #{name} will be executed if defined."
        true
      end
    end

    def add_applied_filters!
      applied_filters_and_rules = content_view.filters.map do |f|
        {
          filter: f,
          rules: f.rules,
          affected_repos: f.applicable_repos.map do |repo|
            repo.slice(:id, :name, :label, :arch, :major, :minor,
                       :content_type, :os_versions, :url, :content_id).merge(redhat: repo.redhat?, root: repo.root.id,
                                                                             product: repo.product.slice(:id, :label))
          end
        }
      end
      self.applied_filters = {
        applied_filters: applied_filters_and_rules,
        dependency_solving: content_view.solve_dependencies
      }
      save!
    end

    def filters_applied?
      # For older content view versions, we do not know if filters were applied.
      # For these, return nil.
      return nil if applied_filters.nil?
      applied_filters["applied_filters"].present?
    end

    def rabl_path
      "katello/api/v2/#{self.class.to_s.demodulize.tableize}/show"
    end

    private

    def remove_environment(env)
      content_view.remove_environment(env) unless content_view.content_view_versions.in_environment(env).count > 1
    end

    def related_resources
      [self.content_view]
    end

    apipie :class, desc: "A class representing #{model_name.human} object" do
      name 'Content View Version'
      refs 'ContentViewVersion'
      sections only: %w[all additional]
      prop_group :katello_basic_props, Katello::Model, meta: { friendly_name: 'Content View Version' }
      property :version, String, desc: 'Returns version of this content view'
    end
    class Jail < ::Safemode::Jail
      allow :name, :label, :version
    end
  end
end