From fe36cf226b8bbc584e3f6bc9a7b7c28d2a0ebd33 Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Tue, 26 May 2026 12:57:31 +0200 Subject: [PATCH 1/7] Fixes #39464 - Stop serializing all hosts in show MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Api::V2::JobInvocationsController#show endpoint serialized every host (via hosts/base) and every template invocation on each request, including the 1-second polling requests from the detail page. None of this data is used by the frontend — hosts are fetched separately by the host table, and per-host template invocations are fetched on row expand. Co-Authored-By: Claude Opus 4.6 --- .../api/v2/job_invocations_controller.rb | 6 +----- .../api/v2/job_invocations/main.json.rabl | 20 ------------------- .../api/v2/job_invocations_controller_test.rb | 14 +------------ 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb index c14b8c158..0e17a47e0 100644 --- a/app/controllers/api/v2/job_invocations_controller.rb +++ b/app/controllers/api/v2/job_invocations_controller.rb @@ -19,14 +19,10 @@ def index api :GET, '/job_invocations/:id', N_('Show job invocation') param :id, :identifier, :required => true - param :host_status, :bool, required: false, desc: N_('Show Job status for the hosts') def show - set_hosts_and_template_invocations + @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id]) @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id]) - if params[:host_status] == 'true' - set_statuses_and_smart_proxies - end end def_param_group :job_invocation do diff --git a/app/views/api/v2/job_invocations/main.json.rabl b/app/views/api/v2/job_invocations/main.json.rabl index 20cf4ac0e..9f26ce09f 100644 --- a/app/views/api/v2/job_invocations/main.json.rabl +++ b/app/views/api/v2/job_invocations/main.json.rabl @@ -18,28 +18,8 @@ end child :targeting do attributes :bookmark_id, :bookmark_name, :search_query, :targeting_type, :user_id, :status, :status_label, :randomized_ordering - - child @hosts => :hosts do - extends 'api/v2/hosts/base' - - if params[:host_status] == 'true' - node :job_status do |host| - @host_statuses[host.id] - end - end - end end child :task do attributes :id, :state, :started_at end - -child @template_invocations do - attributes :template_id, :template_name, :host_id - child :input_values do - attributes :template_input_name, :template_input_id - node :value do |iv| - iv.template_input.respond_to?(:hidden_value) && iv.template_input.hidden_value? ? '*' * 5 : iv.value - end - end -end diff --git a/test/functional/api/v2/job_invocations_controller_test.rb b/test/functional/api/v2/job_invocations_controller_test.rb index 6c5fea2ee..4191da48b 100644 --- a/test/functional/api/v2/job_invocations_controller_test.rb +++ b/test/functional/api/v2/job_invocations_controller_test.rb @@ -27,7 +27,7 @@ class JobInvocationsControllerTest < ActionController::TestCase template = ActiveSupport::JSON.decode(@response.body) assert_not_empty template assert_equal template['job_category'], @invocation.job_category - assert_not_empty template['targeting']['hosts'] + assert_not_nil template['targeting'] end test 'should get invocation detail when taxonomies are set' do @@ -35,18 +35,6 @@ class JobInvocationsControllerTest < ActionController::TestCase get :show, params: taxonomy_params.merge(:id => @invocation.id) assert_response :success end - - test 'should see only permitted hosts' do - @user = FactoryBot.create(:user, admin: false) - @invocation.task.update(user: @user) - setup_user('view', 'job_invocations', nil, @user) - setup_user('view', 'hosts', 'name ~ nope.example.com', @user) - - get :show, params: { :id => @invocation.id }, session: prepare_user(@user) - assert_response :success - response = ActiveSupport::JSON.decode(@response.body) - assert_equal response['targeting']['hosts'], [] - end end context 'creation' do From f0bcdece7cdf345b820f8742abe24dfe1e645b0a Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Thu, 25 Jun 2026 15:20:22 +0200 Subject: [PATCH 2/7] Refs #39464 - Clean up after removing hosts from show - Remove dead @hosts assignment from create action - Extract @pattern_template_invocations into a before_action - Add test assertion for pattern_template_invocations presence Co-Authored-By: Claude Opus 4.6 --- app/controllers/api/v2/job_invocations_controller.rb | 8 +++++--- test/functional/api/v2/job_invocations_controller_test.rb | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb index 0e17a47e0..1574b004e 100644 --- a/app/controllers/api/v2/job_invocations_controller.rb +++ b/app/controllers/api/v2/job_invocations_controller.rb @@ -8,6 +8,7 @@ class JobInvocationsController < ::Api::V2::BaseController before_action :find_optional_nested_object, :only => %w{output raw_output} before_action :find_host, :only => %w{output raw_output} before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs hosts} + before_action :find_pattern_template_invocations, :only => %w{show hosts} wrap_parameters JobInvocation, :include => (JobInvocation.attribute_names + [:ssh]) @@ -20,7 +21,6 @@ def index api :GET, '/job_invocations/:id', N_('Show job invocation') param :id, :identifier, :required => true def show - @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id]) @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id]) end @@ -86,7 +86,6 @@ def create ) composer.trigger! @job_invocation = composer.job_invocation - @hosts = @job_invocation.targeting.hosts process_response @job_invocation rescue JobInvocationComposer::JobTemplateNotFound, JobInvocationComposer::FeatureNotFound => e not_found(error: { message: e.message }) @@ -293,8 +292,11 @@ def parent_scope resource_class.where(nil) end - def set_hosts_and_template_invocations + def find_pattern_template_invocations @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) + end + + def set_hosts_and_template_invocations @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host) if params[:search].present? diff --git a/test/functional/api/v2/job_invocations_controller_test.rb b/test/functional/api/v2/job_invocations_controller_test.rb index 4191da48b..bce05c3fa 100644 --- a/test/functional/api/v2/job_invocations_controller_test.rb +++ b/test/functional/api/v2/job_invocations_controller_test.rb @@ -28,6 +28,7 @@ class JobInvocationsControllerTest < ActionController::TestCase assert_not_empty template assert_equal template['job_category'], @invocation.job_category assert_not_nil template['targeting'] + assert_not_empty template['pattern_template_invocations'] end test 'should get invocation detail when taxonomies are set' do From 3b1f4cda83110f9f8f06c8390ca2906cd922f2db Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Thu, 25 Jun 2026 15:56:49 +0200 Subject: [PATCH 3/7] Refs #39464 - Inline pattern_template_invocations in show The before_action was only used by the show action, so inline the assignment rather than extracting a separate method. Co-Authored-By: Claude Opus 4.6 --- app/controllers/api/v2/job_invocations_controller.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb index 1574b004e..55987509c 100644 --- a/app/controllers/api/v2/job_invocations_controller.rb +++ b/app/controllers/api/v2/job_invocations_controller.rb @@ -8,7 +8,6 @@ class JobInvocationsController < ::Api::V2::BaseController before_action :find_optional_nested_object, :only => %w{output raw_output} before_action :find_host, :only => %w{output raw_output} before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs hosts} - before_action :find_pattern_template_invocations, :only => %w{show hosts} wrap_parameters JobInvocation, :include => (JobInvocation.attribute_names + [:ssh]) @@ -21,6 +20,7 @@ def index api :GET, '/job_invocations/:id', N_('Show job invocation') param :id, :identifier, :required => true def show + @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id]) @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id]) end @@ -292,10 +292,6 @@ def parent_scope resource_class.where(nil) end - def find_pattern_template_invocations - @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) - end - def set_hosts_and_template_invocations @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host) From 18f997cc6b82a7cdd9143d5bc5a890edae6cddf8 Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Tue, 30 Jun 2026 10:22:05 +0200 Subject: [PATCH 4/7] Refs #39464 - Add include_hosts param to avoid breaking public API Co-Authored-By: Claude Sonnet 4.6 --- .../api/v2/job_invocations_controller.rb | 9 ++++++ .../api/v2/job_invocations/main.json.rabl | 24 +++++++++++++++ .../api/v2/job_invocations_controller_test.rb | 30 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb index 55987509c..8dfe88b05 100644 --- a/app/controllers/api/v2/job_invocations_controller.rb +++ b/app/controllers/api/v2/job_invocations_controller.rb @@ -19,10 +19,17 @@ def index api :GET, '/job_invocations/:id', N_('Show job invocation') param :id, :identifier, :required => true + param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility, pass false to skip serializing all hosts.') + param :host_status, :bool, :required => false, :default_value => false, :desc => N_('Show job status for each host, only applicable when include_hosts is true') def show @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id]) @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id]) + + if params[:include_hosts].nil? || Foreman::Cast.to_bool(params[:include_hosts]) + set_hosts_and_template_invocations + set_statuses_and_smart_proxies if Foreman::Cast.to_bool(params[:host_status]) + end end def_param_group :job_invocation do @@ -80,12 +87,14 @@ def show api :POST, '/job_invocations/', N_('Create a job invocation') param_group :job_invocation, :as => :create + param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility, pass false to skip serializing all hosts.') def create composer = JobInvocationComposer.from_api_params( job_invocation_params ) composer.trigger! @job_invocation = composer.job_invocation + set_hosts_and_template_invocations if params[:include_hosts].nil? || Foreman::Cast.to_bool(params[:include_hosts]) process_response @job_invocation rescue JobInvocationComposer::JobTemplateNotFound, JobInvocationComposer::FeatureNotFound => e not_found(error: { message: e.message }) diff --git a/app/views/api/v2/job_invocations/main.json.rabl b/app/views/api/v2/job_invocations/main.json.rabl index 9f26ce09f..3e4111a44 100644 --- a/app/views/api/v2/job_invocations/main.json.rabl +++ b/app/views/api/v2/job_invocations/main.json.rabl @@ -18,8 +18,32 @@ end child :targeting do attributes :bookmark_id, :bookmark_name, :search_query, :targeting_type, :user_id, :status, :status_label, :randomized_ordering + + if @hosts + child @hosts => :hosts do + extends 'api/v2/hosts/base' + + if @host_statuses + node :job_status do |host| + @host_statuses[host.id] + end + end + end + end end child :task do attributes :id, :state, :started_at end + +if @template_invocations + child @template_invocations => :template_invocations do + attributes :template_id, :template_name, :host_id + child :input_values do + attributes :template_input_name, :template_input_id + node :value do |iv| + iv.template_input.respond_to?(:hidden_value) && iv.template_input.hidden_value? ? '*' * 5 : iv.value + end + end + end +end diff --git a/test/functional/api/v2/job_invocations_controller_test.rb b/test/functional/api/v2/job_invocations_controller_test.rb index bce05c3fa..54841b255 100644 --- a/test/functional/api/v2/job_invocations_controller_test.rb +++ b/test/functional/api/v2/job_invocations_controller_test.rb @@ -36,6 +36,22 @@ class JobInvocationsControllerTest < ActionController::TestCase get :show, params: taxonomy_params.merge(:id => @invocation.id) assert_response :success end + + test 'should include hosts by default' do + get :show, params: { :id => @invocation.id } + assert_response :success + result = ActiveSupport::JSON.decode(@response.body) + assert_not_nil result['targeting']['hosts'] + assert_not_nil result['template_invocations'] + end + + test 'should exclude hosts when include_hosts=false' do + get :show, params: { :id => @invocation.id, :include_hosts => false } + assert_response :success + result = ActiveSupport::JSON.decode(@response.body) + assert_nil result['targeting']['hosts'] + assert_nil result['template_invocations'] + end end context 'creation' do @@ -55,6 +71,20 @@ class JobInvocationsControllerTest < ActionController::TestCase assert_response :success end + test 'should include hosts in create response by default' do + post :create, params: { job_invocation: @attrs } + invocation = ActiveSupport::JSON.decode(@response.body) + assert_not_nil invocation['targeting']['hosts'] + assert_response :success + end + + test 'should exclude hosts from create response when include_hosts=false' do + post :create, params: { job_invocation: @attrs, include_hosts: false } + invocation = ActiveSupport::JSON.decode(@response.body) + assert_nil invocation['targeting']['hosts'] + assert_response :success + end + test 'should create with description format overridden' do @attrs[:description_format] = 'format' post :create, params: { job_invocation: @attrs } From 4848d39fe8e042c0c9ae01b28fe7a48359f2b75d Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Tue, 30 Jun 2026 10:25:56 +0200 Subject: [PATCH 5/7] Refs #39464 - Pass include_hosts=false from the frontend Co-Authored-By: Claude Sonnet 4.6 --- webpack/JobInvocationDetail/JobInvocationActions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack/JobInvocationDetail/JobInvocationActions.js b/webpack/JobInvocationDetail/JobInvocationActions.js index a237a527b..131696068 100644 --- a/webpack/JobInvocationDetail/JobInvocationActions.js +++ b/webpack/JobInvocationDetail/JobInvocationActions.js @@ -19,7 +19,7 @@ export const getJobInvocation = url => dispatch => { const fetchData = withInterval( get({ key: JOB_INVOCATION_KEY, - params: { include_permissions: true }, + params: { include_permissions: true, include_hosts: false }, url, handleError: () => { dispatch(stopInterval(JOB_INVOCATION_KEY)); @@ -44,6 +44,7 @@ export const updateJob = jobId => dispatch => { APIActions.get({ url, key: UPDATE_JOB, + params: { include_hosts: false }, }) ); }; From ee1b840744cdcd3ca2434767b8a06b2331717030 Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Wed, 1 Jul 2026 10:00:07 +0200 Subject: [PATCH 6/7] Refs #39464 - Improve test coverage and extract include_hosts? helper - Extract duplicated include_hosts? condition into a private method - Restore dropped 'should see only permitted hosts' authorization test - Add test for host_status=true with include_hosts=false (silent discard) - Add test for host_status=true with include_hosts=true (job_status present) - Merge redundant 'should include hosts by default' into 'should get invocation detail' Co-Authored-By: Claude Sonnet 4.6 --- .../api/v2/job_invocations_controller.rb | 8 +++- .../api/v2/job_invocations_controller_test.rb | 41 ++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb index 8dfe88b05..c119aa4eb 100644 --- a/app/controllers/api/v2/job_invocations_controller.rb +++ b/app/controllers/api/v2/job_invocations_controller.rb @@ -26,7 +26,7 @@ def show @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id]) @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id]) - if params[:include_hosts].nil? || Foreman::Cast.to_bool(params[:include_hosts]) + if include_hosts? set_hosts_and_template_invocations set_statuses_and_smart_proxies if Foreman::Cast.to_bool(params[:host_status]) end @@ -94,7 +94,7 @@ def create ) composer.trigger! @job_invocation = composer.job_invocation - set_hosts_and_template_invocations if params[:include_hosts].nil? || Foreman::Cast.to_bool(params[:include_hosts]) + set_hosts_and_template_invocations if include_hosts? process_response @job_invocation rescue JobInvocationComposer::JobTemplateNotFound, JobInvocationComposer::FeatureNotFound => e not_found(error: { message: e.message }) @@ -313,6 +313,10 @@ def set_hosts_and_template_invocations .includes(:input_values) end + def include_hosts? + params[:include_hosts].nil? || Foreman::Cast.to_bool(params[:include_hosts]) + end + def set_statuses_and_smart_proxies template_invocations = @template_invocations.where(host_id: @hosts.select(:id)) .includes(:run_host_job_task).to_a diff --git a/test/functional/api/v2/job_invocations_controller_test.rb b/test/functional/api/v2/job_invocations_controller_test.rb index 54841b255..2b9d84e2b 100644 --- a/test/functional/api/v2/job_invocations_controller_test.rb +++ b/test/functional/api/v2/job_invocations_controller_test.rb @@ -29,6 +29,8 @@ class JobInvocationsControllerTest < ActionController::TestCase assert_equal template['job_category'], @invocation.job_category assert_not_nil template['targeting'] assert_not_empty template['pattern_template_invocations'] + assert_not_nil template['targeting']['hosts'] + assert_not_nil template['template_invocations'] end test 'should get invocation detail when taxonomies are set' do @@ -37,21 +39,48 @@ class JobInvocationsControllerTest < ActionController::TestCase assert_response :success end - test 'should include hosts by default' do - get :show, params: { :id => @invocation.id } + test 'should exclude hosts when include_hosts=false' do + get :show, params: { :id => @invocation.id, :include_hosts => false } assert_response :success result = ActiveSupport::JSON.decode(@response.body) - assert_not_nil result['targeting']['hosts'] - assert_not_nil result['template_invocations'] + assert_nil result['targeting']['hosts'] + assert_nil result['template_invocations'] end - test 'should exclude hosts when include_hosts=false' do - get :show, params: { :id => @invocation.id, :include_hosts => false } + test 'should see only permitted hosts' do + @user = FactoryBot.create(:user, :admin => false) + @invocation.task.update(:user => @user) + setup_user('view', 'job_invocations', nil, @user) + setup_user('view', 'hosts', 'name ~ nope.example.com', @user) + + get :show, params: { :id => @invocation.id }, session: prepare_user(@user) + assert_response :success + response = ActiveSupport::JSON.decode(@response.body) + assert_equal response['targeting']['hosts'], [] + end + + test 'should ignore host_status when include_hosts=false' do + get :show, params: { :id => @invocation.id, :include_hosts => false, :host_status => true } assert_response :success result = ActiveSupport::JSON.decode(@response.body) assert_nil result['targeting']['hosts'] assert_nil result['template_invocations'] end + + test 'should include job_status per host when host_status=true' do + invocation = FactoryBot.create(:job_invocation, :with_template, :with_task) + invocation.template_invocations << FactoryBot.create(:template_invocation, :with_task, :with_host, :job_invocation => invocation) + invocation.job_category = invocation.pattern_template_invocations.first.template.job_category + invocation.targeting.hosts = invocation.template_invocations.map(&:host) + invocation.save! + + get :show, params: { :id => invocation.id, :include_hosts => true, :host_status => true } + assert_response :success + result = ActiveSupport::JSON.decode(@response.body) + hosts = result['targeting']['hosts'] + assert_not_empty hosts + assert hosts.all? { |h| h.key?('job_status') }, 'Expected job_status to be present on each host' + end end context 'creation' do From 5cfa412ccb9b7888b20d1024ed51e41e28ea6390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Thu, 2 Jul 2026 08:57:24 +0200 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Adam Lazik --- app/controllers/api/v2/job_invocations_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb index c119aa4eb..0bf640111 100644 --- a/app/controllers/api/v2/job_invocations_controller.rb +++ b/app/controllers/api/v2/job_invocations_controller.rb @@ -19,7 +19,7 @@ def index api :GET, '/job_invocations/:id', N_('Show job invocation') param :id, :identifier, :required => true - param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility, pass false to skip serializing all hosts.') + param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility. Pass false to skip serializing all hosts.') param :host_status, :bool, :required => false, :default_value => false, :desc => N_('Show job status for each host, only applicable when include_hosts is true') def show @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values) @@ -87,7 +87,7 @@ def show api :POST, '/job_invocations/', N_('Create a job invocation') param_group :job_invocation, :as => :create - param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility, pass false to skip serializing all hosts.') + param :include_hosts, :bool, :required => false, :default_value => true, :desc => N_('Include hosts and template invocations in the response. Defaults to true for backwards compatibility. Pass false to skip serializing all hosts.') def create composer = JobInvocationComposer.from_api_params( job_invocation_params