Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #37994 - Populate bootc fields from facts and associate hosts with manifest entities #11209

Merged
merged 8 commits into from
Nov 15, 2024
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;
Loading