diff --git a/app/models/katello/concerns/host_managed_extensions.rb b/app/models/katello/concerns/host_managed_extensions.rb index 74c051ed0ec..9438db09b9c 100644 --- a/app/models/katello/concerns/host_managed_extensions.rb +++ b/app/models/katello/concerns/host_managed_extensions.rb @@ -98,6 +98,9 @@ def remote_execution_proxies(provider, *_rest) has_many :content_views, through: :content_view_environments has_many :lifecycle_environments, through: :content_view_environments + has_one :docker_manifest, through: :content_facet, source: :manifest_entity, source_type: 'Katello::DockerManifest' + has_one :docker_manifest_list, through: :content_facet, source: :manifest_entity, source_type: 'Katello::DockerManifestList' + has_many :host_installed_packages, :class_name => "::Katello::HostInstalledPackage", :foreign_key => :host_id, :dependent => :delete_all has_many :installed_packages, :class_name => "::Katello::InstalledPackage", :through => :host_installed_packages diff --git a/app/models/katello/docker_manifest.rb b/app/models/katello/docker_manifest.rb index 70135a4f1fb..4d112fed2c7 100644 --- a/app/models/katello/docker_manifest.rb +++ b/app/models/katello/docker_manifest.rb @@ -5,13 +5,18 @@ class DockerManifest < Katello::Model has_many :docker_manifest_list_manifests, :class_name => "Katello::DockerManifestListManifest", :dependent => :delete_all, :inverse_of => :docker_manifest has_many :docker_manifest_lists, :through => :docker_manifest_list_manifests, :inverse_of => :docker_manifests + has_many :content_facets, :class_name => "::Katello::Host::ContentFacet", :as => :manifest_entity, :dependent => :nullify + has_many :hosts, :class_name => "::Host::Managed", :through => :content_facets, :inverse_of => :docker_manifest CONTENT_TYPE = "docker_manifest".freeze + scope :bootable, -> { where(:is_bootable => true) } + scoped_search :relation => :docker_tags, :on => :name, :rename => :tag, :complete_value => true scoped_search :on => :digest, :rename => :digest, :complete_value => true, :only_explicit => true scoped_search :on => :schema_version, :rename => :schema_version, :complete_value => true, :only_explicit => true scoped_search :relation => :docker_manifest_lists, :on => :digest, :rename => :manifest_list_digest, :complete_value => true, :only_explicit => true + scoped_search :on => :is_bootable, :rename => :bootable, :complete_value => { true => true, false => false }, :only_explicit => true def self.default_sort order(:schema_version) diff --git a/app/models/katello/docker_manifest_list.rb b/app/models/katello/docker_manifest_list.rb index cdd339520ed..de6229a20f5 100644 --- a/app/models/katello/docker_manifest_list.rb +++ b/app/models/katello/docker_manifest_list.rb @@ -6,12 +6,17 @@ class DockerManifestList < Katello::Model has_many :docker_manifest_list_manifests, :class_name => "Katello::DockerManifestListManifest", :dependent => :delete_all, :inverse_of => :docker_manifest_list has_many :docker_manifests, :through => :docker_manifest_list_manifests, :inverse_of => :docker_manifest_lists + has_many :content_facets, :class_name => "::Katello::Host::ContentFacet", :as => :manifest_entity, :dependent => :nullify + has_many :hosts, :class_name => "::Host::Managed", :through => :content_facets, :inverse_of => :docker_manifest_list CONTENT_TYPE = "docker_manifest_list".freeze + scope :bootable, -> { where(:is_bootable => true) } + scoped_search :relation => :docker_tags, :on => :name, :rename => :tag, :complete_value => true scoped_search :on => :digest, :rename => :digest, :complete_value => true, :only_explicit => true scoped_search :on => :schema_version, :rename => :schema_version, :complete_value => true, :only_explicit => true + scoped_search :on => :is_bootable, :rename => :bootable, :complete_value => { true => true, false => false }, :only_explicit => true def self.default_sort order(:schema_version) diff --git a/app/models/katello/host/content_facet.rb b/app/models/katello/host/content_facet.rb index a2566c45bc7..ae25d2fd1e9 100644 --- a/app/models/katello/host/content_facet.rb +++ b/app/models/katello/host/content_facet.rb @@ -15,9 +15,20 @@ class ContentFacet < Katello::Model ALL_TRACER_PACKAGE_NAMES = [ "python-#{HOST_TOOLS_TRACER_PACKAGE_NAME}", "python3-#{HOST_TOOLS_TRACER_PACKAGE_NAME}", HOST_TOOLS_TRACER_PACKAGE_NAME ].freeze + BOOTC_FIELD_FACT_NAMES = [ + "bootc.booted.image", + "bootc.booted.digest", + "bootc.staged.image", + "bootc.staged.digest", + "bootc.rollback.image", + "bootc.rollback.digest", + "bootc.available.image", + "bootc.available.digest", + ].freeze belongs_to :kickstart_repository, :class_name => "::Katello::Repository", :inverse_of => :kickstart_content_facets belongs_to :content_source, :class_name => "::SmartProxy", :inverse_of => :content_facets + belongs_to :manifest_entity, :polymorphic => true, :optional => true, :inverse_of => :content_facets has_many :content_view_environment_content_facets, :class_name => "Katello::ContentViewEnvironmentContentFacet", :dependent => :destroy, :inverse_of => :content_facet has_many :content_view_environments, :through => :content_view_environment_content_facets, @@ -308,6 +319,34 @@ def self.with_non_installable_errata(errata, hosts = nil) Katello::Host::ContentFacet.where(id: non_installable_errata) end + def self.populate_fields_from_facts(host, parser, _type, _source_proxy) + return if host.content_facet.blank? + facet = host.content_facet || host.build_content_facet + attrs_to_add = {} + BOOTC_FIELD_FACT_NAMES.each do |fact_name| + fact_value = parser.facts[fact_name] + field_name = fact_name.tr(".", "_") + attrs_to_add[field_name] = fact_value # overwrite with nil if fact is not present + end + if attrs_to_add['bootc_booted_digest'].present? + manifest_entity = find_manifest_entity(digest: attrs_to_add['bootc_booted_digest']) + if manifest_entity.present? + attrs_to_add['manifest_entity_type'] = manifest_entity.model_name.name + attrs_to_add['manifest_entity_id'] = manifest_entity.id + else + # remove the association if the manifest entity is not found + attrs_to_add['manifest_entity_type'] = nil + attrs_to_add['manifest_entity_id'] = nil + end + end + facet.assign_attributes(attrs_to_add) + facet.save unless facet.new_record? + end + + def self.find_manifest_entity(digest:) + ::Katello::DockerManifestList.find_by(digest: digest) || ::Katello::DockerManifest.find_by(digest: digest) + end + def self.with_applicable_errata(errata) self.joins(:applicable_errata).where("#{Katello::Erratum.table_name}.id" => errata) end diff --git a/app/models/katello/host/subscription_facet.rb b/app/models/katello/host/subscription_facet.rb index 4341e318ab4..44c20f3d0de 100644 --- a/app/models/katello/host/subscription_facet.rb +++ b/app/models/katello/host/subscription_facet.rb @@ -302,10 +302,11 @@ def self.populate_fields_from_facts(host, parser, _type, _source_proxy) return unless host.subscription_facet || has_convert2rhel # Add in custom convert2rhel fact if system was converted using convert2rhel through Katello # We want the value nil unless the custom fact is present otherwise we get a 0 in the database which if debugging - # might make you think it was converted2rhel but not with satellite, that is why I have the tenary below. + # might make you think it was converted2rhel but not with satellite, that is why I have the ternary below. facet = host.subscription_facet || host.build_subscription_facet facet.attributes = { convert2rhel_through_foreman: has_convert2rhel ? ::Foreman::Cast.to_bool(parser.facts['conversions.env.CONVERT2RHEL_THROUGH_FOREMAN']) : nil, + }.compact facet.save unless facet.new_record? end diff --git a/db/migrate/20241112145802_add_manifest_entity_to_content_facets.rb b/db/migrate/20241112145802_add_manifest_entity_to_content_facets.rb new file mode 100644 index 00000000000..3d597d0190e --- /dev/null +++ b/db/migrate/20241112145802_add_manifest_entity_to_content_facets.rb @@ -0,0 +1,7 @@ +class AddManifestEntityToContentFacets < ActiveRecord::Migration[6.1] + def change + add_reference :katello_content_facets, :manifest_entity, polymorphic: true, index: true + change_column_null :katello_content_facets, :manifest_entity_type, true + change_column_null :katello_content_facets, :manifest_entity_id, true + end +end diff --git a/webpack/ForemanColumnExtensions/index.js b/webpack/ForemanColumnExtensions/index.js index e2514dd9e6e..33f6178d2a5 100644 --- a/webpack/ForemanColumnExtensions/index.js +++ b/webpack/ForemanColumnExtensions/index.js @@ -7,13 +7,64 @@ import { PackageIcon, } from '@patternfly/react-icons'; import { Link } from 'react-router-dom'; -import { Flex, FlexItem, Popover, Badge } from '@patternfly/react-core'; +import { + Flex, + FlexItem, + Popover, + Badge, + DescriptionList, + DescriptionListGroup, + DescriptionListDescription as Dd, + DescriptionListTerm as Dt, +} from '@patternfly/react-core'; import { translate as __ } from 'foremanReact/common/I18n'; import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime'; import { ContentViewEnvironmentDisplay } from '../components/extensions/HostDetails/Cards/ContentViewDetailsCard/ContentViewDetailsCard'; import { truncate } from '../utils/helpers'; +import RepoIcon from '../scenes/ContentViews/Details/Repositories/RepoIcon'; const hostsIndexColumnExtensions = [ + { + columnName: 'bootc_booted_image', + title: , + wrapper: (hostDetails) => { + const imageMode = hostDetails?.content_facet_attributes?.bootc_booted_image; + const digest = hostDetails?.content_facet_attributes?.bootc_booted_digest; + return ( + + {imageMode ? + + + +
{__('Booted image')}
+
{hostDetails.content_facet_attributes.bootc_booted_image}
+
+ +
{__('Digest')}
+
{digest}
+
+
+
+ } + > + + + + + : + } + + ); + }, + weight: 35, // between power status (0) and name (50) + isSorted: true, + }, { columnName: 'rhel_lifecycle_status', title: __('RHEL Lifecycle status'), diff --git a/webpack/scenes/ContentViews/Details/Repositories/RepoIcon.js b/webpack/scenes/ContentViews/Details/Repositories/RepoIcon.js index 4ddef68555a..225f074ff95 100644 --- a/webpack/scenes/ContentViews/Details/Repositories/RepoIcon.js +++ b/webpack/scenes/ContentViews/Details/Repositories/RepoIcon.js @@ -3,7 +3,7 @@ import { Tooltip } from '@patternfly/react-core'; import { BundleIcon, MiddlewareIcon, BoxIcon, CodeBranchIcon, FanIcon, TenantIcon, AnsibleTowerIcon } from '@patternfly/react-icons'; import PropTypes from 'prop-types'; -const RepoIcon = ({ type }) => { +const RepoIcon = ({ type, customTooltip }) => { const iconMap = { yum: BundleIcon, docker: MiddlewareIcon, @@ -14,15 +14,17 @@ const RepoIcon = ({ type }) => { }; const Icon = iconMap[type] || BoxIcon; - return {type}}>; + return {customTooltip ?? type}}>; }; RepoIcon.propTypes = { type: PropTypes.string, + customTooltip: PropTypes.string, }; RepoIcon.defaultProps = { type: '', // prevent errors if data isn't loaded yet + customTooltip: null, }; export default RepoIcon;