Skip to content

Commit

Permalink
Fixes #37994 - Populate bootc fields from facts and associate hosts w…
Browse files Browse the repository at this point in the history
…ith manifest entities (#11209)

* Fixes #37994 - populate bootc fields from facts
* Refs #37994 - associate hosts with manifest entities
* Refs #37994 - find_by_image_mode should include hosts with no content facet
  • Loading branch information
jeremylenz authored Nov 15, 2024
1 parent 3f2a522 commit dcf890d
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 5 deletions.
3 changes: 2 additions & 1 deletion app/models/katello/concerns/content_facet_host_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ def find_by_image_mode(_key, _operator, value)
if state
hosts = ::Host::Managed.joins(:content_facet).select(:id).where.not("#{::Katello::Host::ContentFacet.table_name}.bootc_booted_image" => nil)
else
hosts = ::Host::Managed.joins(:content_facet).select(:id).where("#{::Katello::Host::ContentFacet.table_name}.bootc_booted_image" => nil)
# left_outer_joins will include hosts without a content facet. We assume such hosts are package-mode hosts.
hosts = ::Host::Managed.left_outer_joins(:content_facet).select(:id).where("#{::Katello::Host::ContentFacet.table_name}.bootc_booted_image" => nil)
end
{ :conditions => "#{::Host::Managed.table_name}.id IN (#{hosts.to_sql})" }
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/katello/concerns/host_managed_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions app/models/katello/docker_manifest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, :only_explicit => true

def self.default_sort
order(:schema_version)
Expand Down
5 changes: 5 additions & 0 deletions app/models/katello/docker_manifest_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, :only_explicit => true

def self.default_sort
order(:schema_version)
Expand Down
39 changes: 39 additions & 0 deletions app/models/katello/host/content_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/models/katello/host/subscription_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
67 changes: 66 additions & 1 deletion webpack/ForemanColumnExtensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,78 @@ 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,
Text,
TextVariants,
} 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';
import FontAwesomeImageModeIcon from '../components/extensions/Hosts/FontAwesomeImageModeIcon';
import './index.scss';

const hostsIndexColumnExtensions = [
{
columnName: 'bootc_booted_image',
title: (
<span id="image-mode-column-title-icon">
<FontAwesomeImageModeIcon title={__('Image mode / package mode')} />
</span>
),
wrapper: (hostDetails) => {
const imageMode = hostDetails?.content_facet_attributes?.bootc_booted_image;
const digest = hostDetails?.content_facet_attributes?.bootc_booted_digest;
return (
<span className={imageMode ? 'image-mode-column-td-icon' : 'package-mode-column-td-icon'}>
{imageMode ?
<Popover
id="image-mode-tooltip"
className="image-mode-tooltip"
maxWidth="74rem"
headerContent={hostDetails.display_name}
bodyContent={
<Flex direction={{ default: 'column' }}>
<FlexItem>
<Flex direction={{ default: 'row' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem>
<FontAwesomeImageModeIcon />
</FlexItem>
<Text ouiaId="image-mode-popover-h4" component={TextVariants.h4}>{__('Image-mode host')}</Text>
</Flex>
</FlexItem>
<DescriptionList isCompact isHorizontal>
<DescriptionListGroup>
<Dt>{__('Running image')}</Dt>
<Dd>{hostDetails.content_facet_attributes.bootc_booted_image}</Dd>
</DescriptionListGroup>
<DescriptionListGroup>
<Dt>{__('Digest')}</Dt>
<Dd>{digest}</Dd>
</DescriptionListGroup>
</DescriptionList>
</Flex>
}
>
<FontAwesomeImageModeIcon title={__('Image mode')} />
</Popover>
: <span style={{ color: 'var(--pf-global--palette--black-600)' }}><RepoIcon type="yum" customTooltip={__('Package mode')} /></span>
}
</span>
);
},
weight: 35, // between power status (0) and name (50)
isSorted: true,
},
{
columnName: 'rhel_lifecycle_status',
title: __('RHEL Lifecycle status'),
Expand Down
9 changes: 9 additions & 0 deletions webpack/ForemanColumnExtensions/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#image-mode-column-title-icon {
padding: 5px;
}
.image-mode-column-td-icon {
padding: 6px;
}
.package-mode-column-td-icon {
padding: 4px;
}
55 changes: 55 additions & 0 deletions webpack/components/extensions/Hosts/FontAwesomeImageModeIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import propTypes from 'prop-types';
import { Tooltip } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';

const FontAwesomeImageModeIcon = ({ fill, margin, title }) => (
<Tooltip content={title}>
<svg
id="Layer_2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 221.37 221.44"
width="14px"
height="14px"
version="1.1"
xmlSpace="preserve"
style={{
fillRule: 'evenodd',
clipRule: 'evenodd',
strokeLinejoin: 'round',
strokeMiterlimit: 2,
margin: margin || '-2px',
}}
>
<g id="Layer_1-2">
<circle
fill={fill}
className="cls-1"
cx="77.01"
cy="87"
r="20.41"
transform="translate(-39.07 79) rotate(-45)"
/>
<path
className="cls-1"
fill={fill}
d="M205.48,40.09L120.07,1.72c-5.84-2.28-12.28-2.29-18.13-.02L15.93,40.09h0C6.25,43.85,0,52.98,0,63.37v91.2c0,9.48,5.5,18.28,14.02,22.44l85.84,41.91c3.45,1.68,7.2,2.52,10.95,2.52,4.03,0,8.05-.97,11.69-2.89l85.58-45.31c8.2-4.34,13.29-12.8,13.29-22.07V63.35c0-10.36-6.24-19.49-15.88-23.26ZM110.97,28.55l82.09,37.07v60.44l-39.44-37.64c-2.09-2.09-5.48-2.09-7.57,0l-60.43,60.43-24.76-24.76c-2.09-2.09-5.48-2.09-7.57,0l-25,26.93v-85.39L110.97,28.55Z"
/>
</g>
</svg>
</Tooltip>
);

FontAwesomeImageModeIcon.propTypes = {
fill: propTypes.string,
margin: propTypes.string,
title: propTypes.string,
};

FontAwesomeImageModeIcon.defaultProps = {
fill: 'var(--pf-global--palette--black-600)',
margin: '-2px',
title: __('Image mode'),
};

export default FontAwesomeImageModeIcon;
6 changes: 4 additions & 2 deletions webpack/scenes/ContentViews/Details/Repositories/RepoIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,15 +14,17 @@ const RepoIcon = ({ type }) => {
};
const Icon = iconMap[type] || BoxIcon;

return <Tooltip content={<div>{type}</div>}><Icon aria-label={`${type}_type_icon`} /></Tooltip>;
return <Tooltip content={<div>{customTooltip ?? type}</div>}><Icon aria-label={`${type}_type_icon`} /></Tooltip>;
};

RepoIcon.propTypes = {
type: PropTypes.string,
customTooltip: PropTypes.string,
};

RepoIcon.defaultProps = {
type: '', // prevent errors if data isn't loaded yet
customTooltip: null,
};

export default RepoIcon;

0 comments on commit dcf890d

Please sign in to comment.