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;