diff --git a/app/controllers/katello/api/v2/host_bootc_images_controller.rb b/app/controllers/katello/api/v2/host_bootc_images_controller.rb new file mode 100644 index 00000000000..14fce25f507 --- /dev/null +++ b/app/controllers/katello/api/v2/host_bootc_images_controller.rb @@ -0,0 +1,48 @@ +module Katello + class Api::V2::HostBootcImagesController < Api::V2::ApiController + include Katello::Concerns::FilteredAutoCompleteSearch + + resource_description do + api_version 'v2' + api_base_url "/api" + end + + api :GET, "/hosts/bootc_images", N_("List booted bootc container images for hosts") + param :page, :number, :desc => N_("Page number, starting at 1") + param :per_page, :number, :desc => N_("Number of results per page to return") + def bootc_images + bootc_image_map = bootc_host_image_map(params[:search]) + + page = params[:page].to_i || 1 + per_page = params[:per_page].to_i || Setting[:entries_per_page] + paged_images = bootc_image_map.to_a.paginate(page: page, per_page: per_page) + results = paged_images.collect { |image| { image_name: image[0], digests: image[1] } } + render json: { total: bootc_image_map.size, page: page, per_page: per_page, subtotal: bootc_image_map.size, results: results} + end + + private + + def index_relation + query = resource_class.authorized(:view_hosts).distinct + query.joins(:content_facet).where.not(bootc_booted_image: nil, bootc_booted_digest: nil) + query + end + + def resource_class + ::Host::Managed + end + + def bootc_host_image_map(host_search) + # TODO: can this be optimized such that it doesn't require two queries? + content_facets = ::Katello::Host::ContentFacet.where(host_id: ::Host::Managed.joins(:content_facet).search_for(host_search).pluck(:id)) + aggregate_bootc_data = content_facets.where.not(bootc_booted_image: nil, bootc_booted_digest: nil). + select(:bootc_booted_image, :bootc_booted_digest, 'COUNT(hosts.id) as host_count'). + joins(:host).group(:bootc_booted_image, :bootc_booted_digest).order(:bootc_booted_image) + bootc_image_map = Hash.new { |h, k| h[k] = [] } + aggregate_bootc_data.each do |host_image| + bootc_image_map[host_image.bootc_booted_image] << { bootc_booted_digest: host_image.bootc_booted_digest, host_count: host_image.host_count.to_i } + end + bootc_image_map + end + end +end diff --git a/config/routes/api/v2.rb b/config/routes/api/v2.rb index c6c894a76fe..e945334f019 100644 --- a/config/routes/api/v2.rb +++ b/config/routes/api/v2.rb @@ -30,6 +30,10 @@ class ActionDispatch::Routing::Mapper end end + api_resources :host_bootc_images, :only => [:bootc_images] do + get :auto_complete_search, :on => :collection + end + api_resources :capsules, :only => [:index, :show] do member do resource :content, :only => [], :controller => 'capsule_content' do diff --git a/config/routes/overrides.rb b/config/routes/overrides.rb index 142312d6e8d..32bcf9d5fdb 100644 --- a/config/routes/overrides.rb +++ b/config/routes/overrides.rb @@ -59,6 +59,7 @@ def matches?(request) collection do match '/auto_complete_search' => 'host_autocomplete#auto_complete_search', :via => :get + match '/bootc_images' => 'host_bootc_images#bootc_images', :via => :get match '/bulk/add_host_collections' => 'hosts_bulk_actions#bulk_add_host_collections', :via => :put match '/bulk/remove_host_collections' => 'hosts_bulk_actions#bulk_remove_host_collections', :via => :put match '/bulk/remove_host_collections' => 'hosts_bulk_actions#bulk_remove_host_collections', :via => :put diff --git a/test/controllers/api/v2/host_bootc_images_controller_test.rb b/test/controllers/api/v2/host_bootc_images_controller_test.rb new file mode 100644 index 00000000000..ab7b6a4d833 --- /dev/null +++ b/test/controllers/api/v2/host_bootc_images_controller_test.rb @@ -0,0 +1,52 @@ +# encoding: utf-8 + +require "katello_test_helper" + +module Katello + class Api::V2::HostBootcImagesControllerTest < ActionController::TestCase + tests ::Katello::Api::V2::HostBootcImagesController + + def setup + setup_controller_defaults_api + setup_foreman_routes + @host1 = FactoryBot.create(:host, :with_content, :with_subscription, :content_view => katello_content_views(:library_dev_view), + :lifecycle_environment => katello_environments(:library)) + @host2 = FactoryBot.create(:host, :with_content, :with_subscription, :content_view => katello_content_views(:library_dev_view), + :lifecycle_environment => katello_environments(:library)) + @host3 = FactoryBot.create(:host, :with_content, :with_subscription, :content_view => katello_content_views(:library_dev_view), + :lifecycle_environment => katello_environments(:library)) + @host1.content_facet.update!(bootc_booted_image: 'image1') + @host1.content_facet.update!(bootc_booted_digest: 'sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3') + @host2.content_facet.update!(bootc_booted_image: 'image1') + @host2.content_facet.update!(bootc_booted_digest: 'sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3') + @host3.content_facet.update!(bootc_booted_image: 'image2') + @host3.content_facet.update!(bootc_booted_digest: 'sha256:dcfb2965cda67bc3731408aae23dd07ff3116168c2b832e16bba8234525724a5') + end + + def test_bootc_images_counts_properly_no_paging + get :bootc_images + assert_response :success + results = JSON.parse(@response.body)['bootc_images'] + assert_includes results, ["image1", [{"bootc_booted_digest" => "sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3", "host_count" => 2}]] + assert_includes results, ["image2", [{"bootc_booted_digest" => "sha256:dcfb2965cda67bc3731408aae23dd07ff3116168c2b832e16bba8234525724a5", "host_count" => 1}]] + end + + def test_bootc_images_pages + @host2.content_facet.update!(bootc_booted_image: 'image3') + @host2.content_facet.update!(bootc_booted_digest: 'sha256:dcfb2965cda67bd3731408ace93dd07ff3116168c2b832e16bba8234525724c9') + get :bootc_images, params: { page: 1, per_page: 1 } + page1 = @response.body + get :bootc_images, params: { page: 2, per_page: 1 } + page2 = @response.body + get :bootc_images, params: { page: 3, per_page: 1 } + page3 = @response.body + get :bootc_images, params: { page: 4, per_page: 1 } + page4 = @response.body + + assert_equal [["image1", [{"bootc_booted_digest" => "sha256:dcfb2965cda67bd3731408ace23dd07ff3116168c2b832e16bba8234525724a3", "host_count" => 1}]]], JSON.parse(page1)['bootc_images'] + assert_equal [["image2", [{"bootc_booted_digest" => "sha256:dcfb2965cda67bc3731408aae23dd07ff3116168c2b832e16bba8234525724a5", "host_count" => 1}]]], JSON.parse(page2)['bootc_images'] + assert_equal [["image3", [{"bootc_booted_digest" => "sha256:dcfb2965cda67bd3731408ace93dd07ff3116168c2b832e16bba8234525724c9", "host_count" => 1}]]], JSON.parse(page3)['bootc_images'] + assert_empty JSON.parse(page4)['bootc_images'] + end + end +end