diff --git a/app/models/katello/concerns/content_facet_host_extensions.rb b/app/models/katello/concerns/content_facet_host_extensions.rb index 02f6a023778..d08226e18a2 100644 --- a/app/models/katello/concerns/content_facet_host_extensions.rb +++ b/app/models/katello/concerns/content_facet_host_extensions.rb @@ -53,6 +53,17 @@ module ContentFacetHostExtensions scoped_search :relation => :bound_root_repositories, :on => :name, :rename => :repository, :complete_value => true, :ext_method => :find_by_repository_name, :only_explicit => true scoped_search :relation => :bound_content, :on => :label, :rename => :repository_content_label, :complete_value => true, :ext_method => :find_by_repository_content_label, :only_explicit => true + scoped_search relation: :content_facet, on: :bootc_booted_image, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_booted_digest, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_available_image, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_available_digest, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_staged_image, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_staged_digest, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_rollback_image, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_rollback_digest, complete_value: true, only_explicit: true + scoped_search relation: :content_facet, on: :bootc_booted_image, rename: :image_mode, only_explicit: true, ext_method: :find_by_image_mode, + operators: ['='], complete_value: { true: true, false: false} + # preserve options set by facets framework, but add new :reject_if statement accepts_nested_attributes_for( :content_facet, @@ -77,6 +88,17 @@ def content_facet_ignore_update?(attributes) end module ClassMethods + def find_by_image_mode(_key, _operator, value) + # operator is always '=' + state = ::Foreman::Cast.to_bool(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) + end + { :conditions => "#{::Host::Managed.table_name}.id IN (#{hosts.to_sql})" } + end + def find_by_applicable_errata(_key, operator, value) conditions = sanitize_sql_for_conditions(["#{Katello::Erratum.table_name}.errata_id #{operator} ?", value_to_sql(operator, value)]) hosts = ::Host::Managed.joins(:applicable_errata).where(conditions) diff --git a/app/models/katello/concerns/host_managed_extensions.rb b/app/models/katello/concerns/host_managed_extensions.rb index 1e4fae637b5..ce2bf0a8e66 100644 --- a/app/models/katello/concerns/host_managed_extensions.rb +++ b/app/models/katello/concerns/host_managed_extensions.rb @@ -89,7 +89,7 @@ def remote_execution_proxies(provider, *_rest) prepend Overrides delegate :content_source_id, :single_content_view, :single_lifecycle_environment, :default_environment?, :single_content_view_environment?, :multi_content_view_environment?, :kickstart_repository_id, :bound_repositories, - :installable_errata, :installable_rpms, to: :content_facet, allow_nil: true + :installable_errata, :installable_rpms, :image_mode_host?, to: :content_facet, allow_nil: true delegate :release_version, :purpose_role, :purpose_usage, to: :subscription_facet, allow_nil: true @@ -128,6 +128,10 @@ def remote_execution_proxies(provider, *_rest) scope :with_pools_expiring_in_days, ->(days) { joins(:pools).merge(Katello::Pool.expiring_in_days(days)).distinct } + scope :image_mode, -> do + joins(:content_facet).where.not("#{::Katello::Host::ContentFacet.table_name}.bootc_booted_image" => nil) + end + scoped_search :relation => :host_collections, :on => :id, :complete_value => false, :rename => :host_collection_id, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER scoped_search :relation => :host_collections, :on => :name, :complete_value => true, :rename => :host_collection scoped_search :relation => :installed_packages, :on => :nvra, :complete_value => true, :rename => :installed_package, :only_explicit => true diff --git a/app/models/katello/host/content_facet.rb b/app/models/katello/host/content_facet.rb index 310934c4543..5894e288143 100644 --- a/app/models/katello/host/content_facet.rb +++ b/app/models/katello/host/content_facet.rb @@ -47,6 +47,8 @@ class ContentFacet < Katello::Model validates_with Katello::Validators::GeneratedContentViewValidator validates_associated :content_view_environment_content_facets, :message => _("invalid: The content source must sync the lifecycle environment assigned to the host. See the logs for more information.") validates :host, :presence => true, :allow_blank => false + validates :bootc_booted_digest, :bootc_available_digest, :bootc_staged_digest, :bootc_rollback_digest, + format: { with: /\Asha256:[A-Fa-f0-9]{64}\z/, message: "must be a valid sha256 digest", allow_nil: true } scope :with_environments, ->(lifecycle_environments) do joins(:content_view_environment_content_facets => :content_view_environment). @@ -88,6 +90,10 @@ def mark_cves_unchanged self.cves_changed = false end + def image_mode_host? + bootc_booted_image.present? + end + def cves_changed? cves_changed end diff --git a/app/views/katello/api/v2/content_facet/base.json.rabl b/app/views/katello/api/v2/content_facet/base.json.rabl index 2f1cec206fc..7a3616f805a 100644 --- a/app/views/katello/api/v2/content_facet/base.json.rabl +++ b/app/views/katello/api/v2/content_facet/base.json.rabl @@ -67,3 +67,6 @@ end child :kickstart_repository => :kickstart_repository do attributes :id, :name end + +attributes :bootc_booted_image, :bootc_booted_digest, :bootc_available_image, :bootc_available_digest, + :bootc_staged_image, :bootc_staged_digest, :bootc_rollback_image, :bootc_rollback_digest diff --git a/db/migrate/20241007212705_add_bootc_facts_to_content_facet.rb b/db/migrate/20241007212705_add_bootc_facts_to_content_facet.rb new file mode 100644 index 00000000000..0fde16f11b5 --- /dev/null +++ b/db/migrate/20241007212705_add_bootc_facts_to_content_facet.rb @@ -0,0 +1,27 @@ +class AddBootcFactsToContentFacet < ActiveRecord::Migration[6.1] + def change + add_column :katello_content_facets, :bootc_booted_image, :string + add_column :katello_content_facets, :bootc_booted_digest, :string + + add_column :katello_content_facets, :bootc_available_image, :string + add_column :katello_content_facets, :bootc_available_digest, :string + + add_column :katello_content_facets, :bootc_staged_image, :string + add_column :katello_content_facets, :bootc_staged_digest, :string + + add_column :katello_content_facets, :bootc_rollback_image, :string + add_column :katello_content_facets, :bootc_rollback_digest, :string + + add_index :katello_content_facets, :bootc_booted_image + add_index :katello_content_facets, :bootc_booted_digest + + add_index :katello_content_facets, :bootc_available_image + add_index :katello_content_facets, :bootc_available_digest + + add_index :katello_content_facets, :bootc_staged_image + add_index :katello_content_facets, :bootc_staged_digest + + add_index :katello_content_facets, :bootc_rollback_image + add_index :katello_content_facets, :bootc_rollback_digest + end +end diff --git a/test/models/concerns/host_managed_extensions_test.rb b/test/models/concerns/host_managed_extensions_test.rb index 0fe78ac19b3..73c38f90889 100644 --- a/test/models/concerns/host_managed_extensions_test.rb +++ b/test/models/concerns/host_managed_extensions_test.rb @@ -19,6 +19,18 @@ def setup end class HostManagedExtensionsTest < HostManagedExtensionsTestBase + def test_image_mode_host_positive + Support::HostSupport.attach_content_facet(@foreman_host, @view, @library) + @foreman_host.content_facet.update(bootc_booted_image: 'quay.io/salami/salad') + assert @foreman_host.content_facet.image_mode_host? + end + + def test_image_mode_host_negative + Support::HostSupport.attach_content_facet(@foreman_host, @view, @library) + @foreman_host.content_facet.update(bootc_booted_image: nil) + refute @foreman_host.content_facet.image_mode_host? + end + def test_update_organization host = FactoryBot.create(:host, :with_subscription) assert_raises ::Katello::Errors::HostRegisteredException do @@ -58,6 +70,55 @@ def test_pools_expiring_in_days assert_includes ::Host.search_for('pools_expiring_in_days = 30'), host_with_pool end + def test_image_mode_search + host_no_image = FactoryBot.create(:host, :with_content, :with_subscription, :content_view => @library_view, :lifecycle_environment => @library) + Support::HostSupport.attach_content_facet(@foreman_host, @view, @library) + @foreman_host.content_facet.update(bootc_booted_image: 'quay.io/salami/soup') + assert_includes ::Host.search_for('image_mode = true'), @foreman_host + refute_includes ::Host.search_for('image_mode = true'), host_no_image + assert_includes ::Host.search_for('image_mode = false'), host_no_image + refute_includes ::Host.search_for('image_mode = false'), @foreman_host + end + + def test_bootc_facts_search + host_no_image = FactoryBot.create(:host, :with_content, :with_subscription, :content_view => @library_view, :lifecycle_environment => @library) + Support::HostSupport.attach_content_facet(@foreman_host, @view, @library) + @foreman_host.content_facet.update( + bootc_booted_image: 'quay.io/salami/booted', + bootc_booted_digest: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + bootc_available_image: 'quay.io/salami/available', + bootc_available_digest: 'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + bootc_staged_image: 'quay.io/salami/staged', + bootc_staged_digest: 'sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321', + bootc_rollback_image: 'quay.io/salami/rollback', + bootc_rollback_digest: 'sha256:0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba' + ) + + assert_includes ::Host.search_for('bootc_booted_image = quay.io/salami/booted'), @foreman_host + refute_includes ::Host.search_for('bootc_booted_image = quay.io/salami/booted'), host_no_image + + assert_includes ::Host.search_for('bootc_booted_digest = sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'), @foreman_host + refute_includes ::Host.search_for('bootc_booted_digest = sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'), host_no_image + + assert_includes ::Host.search_for('bootc_available_image = quay.io/salami/available'), @foreman_host + refute_includes ::Host.search_for('bootc_available_image = quay.io/salami/available'), host_no_image + + assert_includes ::Host.search_for('bootc_available_digest = sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'), @foreman_host + refute_includes ::Host.search_for('bootc_available_digest = sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'), host_no_image + + assert_includes ::Host.search_for('bootc_staged_image = quay.io/salami/staged'), @foreman_host + refute_includes ::Host.search_for('bootc_staged_image = quay.io/salami/staged'), host_no_image + + assert_includes ::Host.search_for('bootc_staged_digest = sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321'), @foreman_host + refute_includes ::Host.search_for('bootc_staged_digest = sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321'), host_no_image + + assert_includes ::Host.search_for('bootc_rollback_image = quay.io/salami/rollback'), @foreman_host + refute_includes ::Host.search_for('bootc_rollback_image = quay.io/salami/rollback'), host_no_image + + assert_includes ::Host.search_for('bootc_rollback_digest = sha256:0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba'), @foreman_host + refute_includes ::Host.search_for('bootc_rollback_digest = sha256:0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba'), host_no_image + end + def test_smart_proxy_ids_with_katello content_source = FactoryBot.create(:smart_proxy, :features => [Feature.where(:name => "Pulp Node").first_or_create]) diff --git a/test/models/host/content_facet_test.rb b/test/models/host/content_facet_test.rb index 9cf0ed54d27..a528c295cc9 100644 --- a/test/models/host/content_facet_test.rb +++ b/test/models/host/content_facet_test.rb @@ -27,6 +27,58 @@ def test_content_view_version assert_equal view.version(library), host.content_facet.content_view_environments.reload.first.content_view_version end + def test_bootc_booted_digest_validation + error = assert_raises(ActiveRecord::RecordInvalid) do + host.content_facet.update!(bootc_booted_digest: "blah") + end + assert_includes error.message, "Bootc booted digest must be a valid sha256 digest" + end + + def test_bootc_available_digest_validation + error = assert_raises(ActiveRecord::RecordInvalid) do + host.content_facet.update!(bootc_available_digest: "blah") + end + assert_includes error.message, "Bootc available digest must be a valid sha256 digest" + end + + def test_bootc_staged_digest_validation + error = assert_raises(ActiveRecord::RecordInvalid) do + host.content_facet.update!(bootc_staged_digest: "blah") + end + assert_includes error.message, "Bootc staged digest must be a valid sha256 digest" + end + + def test_bootc_rollback_digest_validation + error = assert_raises(ActiveRecord::RecordInvalid) do + host.content_facet.update!(bootc_rollback_digest: "blah") + end + assert_includes error.message, "Bootc rollback digest must be a valid sha256 digest" + end + + def test_bootc_booted_digest_valid + valid_digest = "sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3" + host.content_facet.update!(bootc_booted_digest: valid_digest) + assert_equal valid_digest, host.content_facet.bootc_booted_digest + end + + def test_bootc_available_digest_valid + valid_digest = "sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3" + host.content_facet.update!(bootc_available_digest: valid_digest) + assert_equal valid_digest, host.content_facet.bootc_available_digest + end + + def test_bootc_staged_digest_valid + valid_digest = "sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3" + host.content_facet.update!(bootc_staged_digest: valid_digest) + assert_equal valid_digest, host.content_facet.bootc_staged_digest + end + + def test_bootc_rollback_digest_valid + valid_digest = "sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3" + host.content_facet.update!(bootc_rollback_digest: valid_digest) + assert_equal valid_digest, host.content_facet.bootc_rollback_digest + end + def test_tracer_installed? refute host.content_facet.tracer_installed?