diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d8b4ab7a415..c5351b686db 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -85,8 +85,6 @@ Lint/UnusedMethodArgument: Lint/UselessMethodDefinition: Exclude: - 'app/messages/route_destination_update_message.rb' - - 'app/presenters/v3/buildpack_presenter.rb' - - 'app/presenters/v3/process_presenter.rb' - 'spec/support/fake_front_controller.rb' # Offense count: 791 diff --git a/app/controllers/v3/processes_controller.rb b/app/controllers/v3/processes_controller.rb index ecfdce92dfc..f3c6a6aa31f 100644 --- a/app/controllers/v3/processes_controller.rb +++ b/app/controllers/v3/processes_controller.rb @@ -1,6 +1,7 @@ require 'presenters/v3/paginated_list_presenter' require 'presenters/v3/process_presenter' require 'presenters/v3/process_stats_presenter' +require 'presenters/v3/process_instances_presenter' require 'cloud_controller/paging/pagination_options' require 'actions/process_delete' require 'fetchers/process_list_fetcher' @@ -106,6 +107,12 @@ def stats render status: :ok, json: Presenters::V3::ProcessStatsPresenter.new(@process.type, process_stats) end + def instances + instances = instances_reporters.instances_for_processes([@process]) + + render status: :ok, json: Presenters::V3::ProcessInstancesPresenter.new(@process, instances[@process.guid]) + end + private def find_process_and_space diff --git a/app/messages/processes_list_message.rb b/app/messages/processes_list_message.rb index 270e3380ea2..cf2d4165679 100644 --- a/app/messages/processes_list_message.rb +++ b/app/messages/processes_list_message.rb @@ -12,6 +12,8 @@ class ProcessesListMessage < MetadataListMessage validates_with NoAdditionalParamsValidator # from BaseMessage + # validates :space_guids, array: true, allow_nil: true + # validates :organization_guids, array: true, allow_nil: true validates :app_guids, array: true, allow_nil: true validate :app_nested_request, if: -> { app_guid.present? } diff --git a/app/presenters/v3/buildpack_presenter.rb b/app/presenters/v3/buildpack_presenter.rb index ec32547b700..7d004beaa83 100644 --- a/app/presenters/v3/buildpack_presenter.rb +++ b/app/presenters/v3/buildpack_presenter.rb @@ -26,13 +26,6 @@ def to_hash } end - class << self - # :labels and :annotations come from MetadataPresentationHelpers - def associated_resources - super - end - end - private def buildpack diff --git a/app/presenters/v3/process_instances_presenter.rb b/app/presenters/v3/process_instances_presenter.rb new file mode 100644 index 00000000000..446c078f151 --- /dev/null +++ b/app/presenters/v3/process_instances_presenter.rb @@ -0,0 +1,47 @@ +require 'presenters/v3/base_presenter' +require 'presenters/mixins/metadata_presentation_helpers' + +module VCAP::CloudController + module Presenters + module V3 + class ProcessInstancesPresenter < BasePresenter + attr_reader :instances + + def initialize(process, instances) + super(process) + @instances = instances + end + + def to_hash + { + resources: build_instances, + links: build_links + } + end + + private + + def process + @resource + end + + def build_instances + instances.map do |index, instance| + { + index: index, + state: instance[:state], + since: instance[:since] + } + end + end + + def build_links + { + self: { href: url_builder.build_url(path: "/v3/processes/#{process.guid}/instances") }, + process: { href: url_builder.build_url(path: "/v3/processes/#{process.guid}") } + } + end + end + end + end +end diff --git a/app/presenters/v3/process_presenter.rb b/app/presenters/v3/process_presenter.rb index b83f955f3ba..a6c2bfd759f 100644 --- a/app/presenters/v3/process_presenter.rb +++ b/app/presenters/v3/process_presenter.rb @@ -8,13 +8,6 @@ module V3 class ProcessPresenter < BasePresenter include VCAP::CloudController::Presenters::Mixins::MetadataPresentationHelpers - class << self - # :labels and :annotations come from MetadataPresentationHelpers - def associated_resources - super - end - end - def to_hash health_check_data = { timeout: process.health_check_timeout, invocation_timeout: process.health_check_invocation_timeout, interval: process.health_check_interval } health_check_data[:endpoint] = process.health_check_http_endpoint if process.health_check_type == HealthCheckTypes::HTTP diff --git a/config/routes.rb b/config/routes.rb index ccdd5788eaf..5648c7ed427 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,7 @@ get '/processes', to: 'processes#index' get '/processes/:process_guid', to: 'processes#show' patch '/processes/:process_guid', to: 'processes#update' + get '/processes/:process_guid/instances', to: 'processes#instances' delete '/processes/:process_guid/instances/:index', to: 'processes#terminate' post '/processes/:process_guid/actions/scale', to: 'processes#scale' get '/processes/:process_guid/stats', to: 'processes#stats' diff --git a/lib/cloud_controller/backends/instances_reporters.rb b/lib/cloud_controller/backends/instances_reporters.rb index 6c9db092857..0210c5326e7 100644 --- a/lib/cloud_controller/backends/instances_reporters.rb +++ b/lib/cloud_controller/backends/instances_reporters.rb @@ -28,6 +28,7 @@ def stats_for_app(app) end delegate :number_of_starting_and_running_instances_for_processes, :instance_count_summary, to: :diego_reporter + delegate :instances_for_processes, to: :diego_stats_reporter private diff --git a/lib/cloud_controller/diego/bbs_instances_client.rb b/lib/cloud_controller/diego/bbs_instances_client.rb index aff9cd5f897..2a878ef1c1e 100644 --- a/lib/cloud_controller/diego/bbs_instances_client.rb +++ b/lib/cloud_controller/diego/bbs_instances_client.rb @@ -11,7 +11,7 @@ def lrp_instances(process) process_guid = ProcessGuid.from_process(process) logger.info('lrp.instances.request', process_guid:) - actual_lrps_response = handle_diego_errors(process_guid) do + actual_lrps_response = handle_diego_errors do response = @client.actual_lrps_by_process_guid(process_guid) logger.info('lrp.instances.response', process_guid: process_guid, error: response.error) response @@ -20,9 +20,22 @@ def lrp_instances(process) actual_lrps_response.actual_lrps end + def actual_lrps_by_processes(processes) + process_guids = processes.map { |process| ProcessGuid.from_process(process) } + logger.info('actual.lrps.by.processes.request', process_guids:) + + actual_lrps_response = handle_diego_errors do + response = @client.actual_lrps_by_process_guids(process_guids) + logger.info('actual.lrps.by.processes.response', process_guids: process_guids, error: response.error) + response + end + + actual_lrps_response.actual_lrps + end + def desired_lrp_instance(process) process_guid = ProcessGuid.from_process(process) - response = handle_diego_errors(process_guid) do + response = handle_diego_errors(handle_resource_not_found: true, process_guid: process_guid) do @client.desired_lrp_by_process_guid(process_guid) end response.desired_lrp @@ -30,7 +43,7 @@ def desired_lrp_instance(process) private - def handle_diego_errors(process_guid) + def handle_diego_errors(handle_resource_not_found: false, process_guid: nil) begin response = yield rescue ::Diego::Error => e @@ -38,12 +51,11 @@ def handle_diego_errors(process_guid) end if response.error - if response.error.type == ::Diego::Bbs::ErrorTypes::ResourceNotFound + if handle_resource_not_found && response.error.type == ::Diego::Bbs::ErrorTypes::ResourceNotFound raise CloudController::Errors::NoRunningInstances.new("No running instances found for process guid #{process_guid}") end raise CloudController::Errors::InstancesUnavailable.new(response.error.message) - end response diff --git a/lib/cloud_controller/diego/reporters/instances_stats_reporter.rb b/lib/cloud_controller/diego/reporters/instances_stats_reporter.rb index c4ea6e4eb73..802afe4bb1e 100644 --- a/lib/cloud_controller/diego/reporters/instances_stats_reporter.rb +++ b/lib/cloud_controller/diego/reporters/instances_stats_reporter.rb @@ -29,6 +29,41 @@ def stats_for_app(process) raise exception end + def instances_for_processes(processes) + logger.debug('instances_for_processes.fetching_actual_lrps') + + # Fetch actual_lrps for all processes + actual_lrps = bbs_instances_client.actual_lrps_by_processes(processes) + + lrps_by_process_guid = actual_lrps.group_by { |lrp| (pg = lrp.actual_lrp_key&.process_guid) && ProcessGuid.cc_process_guid(pg) } + + current_time_since_epoch_ns = Time.now.to_f * 1e9 + results = {} + processes.each do |process| + newest_lrp_by_index = (lrps_by_process_guid[process.guid] || []). + group_by { |lrp| lrp.actual_lrp_key&.index }. + transform_values { |lrps| lrps.max_by { |lrp| lrp.since || 0 } } + + instances = {} + # Fill in the instances up to the max of desired instances and actual instances + [process.instances, newest_lrp_by_index.length].max.times do |idx| + lrp = newest_lrp_by_index[idx] + instances[idx] = if lrp + { + state: LrpStateTranslator.translate_lrp_state(lrp), + since: nanoseconds_to_seconds(current_time_since_epoch_ns - lrp.since) + } + else + { state: VCAP::CloudController::Diego::LRP_DOWN } + end + end + + results[process.guid] = instances + end + + results + end + private attr_reader :bbs_instances_client diff --git a/lib/diego/client.rb b/lib/diego/client.rb index b310bdc405d..790c6b63376 100644 --- a/lib/diego/client.rb +++ b/lib/diego/client.rb @@ -152,6 +152,17 @@ def actual_lrps_by_process_guid(process_guid) protobuf_decode!(response.body, Bbs::Models::ActualLRPsResponse) end + def actual_lrps_by_process_guids(process_guids) + request = protobuf_encode!({ process_guids: }, Bbs::Models::ActualLRPsByProcessGuidsRequest) + + response = with_request_error_handling do + client.post(Routes::ACTUAL_LRPS_BY_PROCESS_GUIDS, request, headers) + end + + validate_status_200!(response) + protobuf_decode!(response.body, Bbs::Models::ActualLRPsByProcessGuidsResponse) + end + def with_request_error_handling delay = 0.25 max_delay = 5 diff --git a/lib/diego/routes.rb b/lib/diego/routes.rb index a38de4c3852..e1f194c5c3b 100644 --- a/lib/diego/routes.rb +++ b/lib/diego/routes.rb @@ -13,5 +13,6 @@ module Routes REMOVE_DESIRED_LRP = '/v1/desired_lrp/remove'.freeze RETIRE_ACTUAL_LRP = '/v1/actual_lrps/retire'.freeze ACTUAL_LRPS = '/v1/actual_lrps/list'.freeze + ACTUAL_LRPS_BY_PROCESS_GUIDS = '/v1/actual_lrps/list_by_process_guids'.freeze end end diff --git a/spec/diego/client_spec.rb b/spec/diego/client_spec.rb index aadcb821293..720225f88a1 100644 --- a/spec/diego/client_spec.rb +++ b/spec/diego/client_spec.rb @@ -668,6 +668,34 @@ module Diego end end + describe '#actual_lrps_by_process_guids' do + let(:actual_lrps) { [::Diego::Bbs::Models::ActualLRP.new, ::Diego::Bbs::Models::ActualLRP.new] } + let(:response_status) { 200 } + let(:response_body) do + Bbs::Models::ActualLRPsByProcessGuidsResponse.encode( + Bbs::Models::ActualLRPsByProcessGuidsResponse.new(error: nil, actual_lrps: actual_lrps) + ).to_s + end + let(:process_guids) { %w[process-guid another-process-guid] } + + before do + stub_request(:post, "#{bbs_url}/v1/actual_lrps/list_by_process_guids").to_return(status: response_status, body: response_body) + end + + it 'returns a LRP instances by process_guids response' do + expected_request = Bbs::Models::ActualLRPsByProcessGuidsRequest.new(process_guids:) + + response = client.actual_lrps_by_process_guids(process_guids) + expect(response).to be_a(Bbs::Models::ActualLRPsByProcessGuidsResponse) + expect(response.error).to be_nil + expect(response.actual_lrps).to eq(actual_lrps) + expect(a_request(:post, "#{bbs_url}/v1/actual_lrps/list_by_process_guids").with( + body: Bbs::Models::ActualLRPsByProcessGuidsRequest.encode(expected_request).to_s, + headers: { 'Content-Type' => 'application/x-protobuf', 'X-Vcap-Request-Id' => request_id } + )).to have_been_made.once + end + end + describe '#desired_lrps_scheduling_infos' do let(:scheduling_infos) { [::Diego::Bbs::Models::DesiredLRPSchedulingInfo.new] } let(:response_body) do diff --git a/spec/request/processes_spec.rb b/spec/request/processes_spec.rb index 5e647610b9c..349a573f234 100644 --- a/spec/request/processes_spec.rb +++ b/spec/request/processes_spec.rb @@ -657,6 +657,77 @@ end end + describe 'GET /v3/processes/:guid/instances' do + let(:process) { VCAP::CloudController::ProcessModel.make(:process, app: app_model) } + let(:two_days_ago_since_epoch_ns) { 2.days.ago.to_f * 1e9 } + let(:two_days_in_seconds) { 60 * 60 * 24 * 2 } + let(:second_in_ns) { 1_000_000_000 } + let(:actual_lrp_0) do + Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: Diego::Bbs::Models::ActualLRPKey.new(process_guid: process.guid + 'version', index: 0), + actual_lrp_instance_key: Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-a'), + state: Diego::ActualLRPState::RUNNING, + placement_error: '', + since: two_days_ago_since_epoch_ns + ) + end + let(:actual_lrp_1) do + Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: Diego::Bbs::Models::ActualLRPKey.new(process_guid: process.guid + 'version', index: 1), + actual_lrp_instance_key: Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-b'), + state: Diego::ActualLRPState::CLAIMED, + placement_error: '', + since: two_days_ago_since_epoch_ns + (1 * second_in_ns) + ) + end + let(:bbs_response) { Diego::Bbs::Models::ActualLRPsByProcessGuidsResponse.new(actual_lrps: [actual_lrp_0, actual_lrp_1]) } + let(:bbs_client) { double(:bbs_client) } + + let(:expected_response) do + { + 'resources' => [{ + 'index' => 0, + 'state' => 'RUNNING', + 'since' => two_days_in_seconds + }, { + 'index' => 1, + 'state' => 'STARTING', + 'since' => two_days_in_seconds - 1 + }], + 'links' => { + 'self' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}/instances" }, + 'process' => { 'href' => "#{link_prefix}/v3/processes/#{process.guid}" } + } + } + end + + before do + CloudController::DependencyLocator.instance.register(:bbs_instances_client, VCAP::CloudController::Diego::BbsInstancesClient.new(bbs_client)) + allow(bbs_client).to receive(:actual_lrps_by_process_guids).and_return(bbs_response) + end + + it 'retrieves all instances for the process' do + get "/v3/processes/#{process.guid}/instances", nil, developer_headers + + parsed_response = Oj.load(last_response.body) + + expect(last_response.status).to eq(200) + expect(parsed_response).to be_a_response_like(expected_response) + end + + context 'permissions' do + let(:api_call) { ->(user_headers) { get "/v3/processes/#{process.guid}/instances", nil, user_headers } } + + let(:expected_codes_and_responses) do + h = Hash.new({ code: 200, response_object: expected_response }.freeze) + h['org_auditor'] = h['org_billing_manager'] = h['no_role'] = { code: 404, response_object: [] } + h + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + end + describe 'PATCH /v3/processes/:guid' do let(:revision) { VCAP::CloudController::RevisionModel.make } let(:process) do diff --git a/spec/unit/lib/cloud_controller/diego/bbs_instances_client_spec.rb b/spec/unit/lib/cloud_controller/diego/bbs_instances_client_spec.rb index 3dcbb78fbf5..9ca05948828 100644 --- a/spec/unit/lib/cloud_controller/diego/bbs_instances_client_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/bbs_instances_client_spec.rb @@ -21,41 +21,67 @@ module VCAP::CloudController::Diego expect(bbs_client).to have_received(:actual_lrps_by_process_guid).with(process_guid) end - context 'when the response contains a ResourceNotFound error' do + context 'when a Diego error is thrown' do + before do + allow(bbs_client).to receive(:actual_lrps_by_process_guid).with(process_guid).and_raise(::Diego::Error.new('boom')) + end + + it 're-raises with a CC Error' do + expect do + client.lrp_instances(process) + end.to raise_error(CloudController::Errors::InstancesUnavailable, 'boom') + end + end + + context 'when the response contains an unknown error' do let(:bbs_response) do - ::Diego::Bbs::Models::ActualLRPGroupsResponse.new(error: ::Diego::Bbs::Models::Error.new( - message: 'error-message', - type: ::Diego::Bbs::Models::Error::Type::ResourceNotFound - )) + ::Diego::Bbs::Models::ActualLRPsResponse.new(error: ::Diego::Bbs::Models::Error.new(message: 'error-message')) end it 'raises' do expect do client.lrp_instances(process) - end.to raise_error(CloudController::Errors::NoRunningInstances) + end.to raise_error(CloudController::Errors::InstancesUnavailable, 'error-message') end end + end + + describe '#actual_lrps_by_processes' do + let(:processes) { [VCAP::CloudController::ProcessModelFactory.make] } + let(:process_guids) { [ProcessGuid.from_process(processes[0])] } + let(:actual_lrp) { ::Diego::Bbs::Models::ActualLRP.new(state: 'potato') } + let(:actual_lrps) { [actual_lrp] } + let(:bbs_response) { ::Diego::Bbs::Models::ActualLRPsByProcessGuidsResponse.new(actual_lrps:) } + + before do + allow(bbs_client).to receive(:actual_lrps_by_process_guids).with(process_guids).and_return(bbs_response) + end + + it 'sends the lrp instances py process_guids request to diego' do + client.actual_lrps_by_processes(processes) + expect(bbs_client).to have_received(:actual_lrps_by_process_guids).with(process_guids) + end context 'when a Diego error is thrown' do before do - allow(bbs_client).to receive(:actual_lrps_by_process_guid).with(process_guid).and_raise(::Diego::Error.new('boom')) + allow(bbs_client).to receive(:actual_lrps_by_process_guids).with(process_guids).and_raise(::Diego::Error.new('boom')) end it 're-raises with a CC Error' do expect do - client.lrp_instances(process) + client.actual_lrps_by_processes(processes) end.to raise_error(CloudController::Errors::InstancesUnavailable, 'boom') end end context 'when the response contains an unknown error' do let(:bbs_response) do - ::Diego::Bbs::Models::ActualLRPsResponse.new(error: ::Diego::Bbs::Models::Error.new(message: 'error-message')) + ::Diego::Bbs::Models::ActualLRPsByProcessGuidsResponse.new(error: ::Diego::Bbs::Models::Error.new(message: 'error-message')) end it 'raises' do expect do - client.lrp_instances(process) + client.actual_lrps_by_processes(processes) end.to raise_error(CloudController::Errors::InstancesUnavailable, 'error-message') end end @@ -83,7 +109,7 @@ module VCAP::CloudController::Diego context 'when the response contains a ResourceNotFound error' do let(:bbs_response) do - ::Diego::Bbs::Models::ActualLRPGroupsResponse.new(error: ::Diego::Bbs::Models::Error.new( + ::Diego::Bbs::Models::DesiredLRPResponse.new(error: ::Diego::Bbs::Models::Error.new( message: 'error-message', type: ::Diego::Bbs::Models::Error::Type::ResourceNotFound )) diff --git a/spec/unit/lib/cloud_controller/diego/reporters/instances_stats_reporter_spec.rb b/spec/unit/lib/cloud_controller/diego/reporters/instances_stats_reporter_spec.rb index 2c702118907..aff3962a0bb 100644 --- a/spec/unit/lib/cloud_controller/diego/reporters/instances_stats_reporter_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/reporters/instances_stats_reporter_spec.rb @@ -692,6 +692,161 @@ def make_actual_lrp(instance_guid:, index:, state:, error:, since:) end end end + + describe '#instances_for_processes' do + let(:second_in_ns) { 1_000_000_000 } + let(:actual_lrp_0) do + ::Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: ::Diego::Bbs::Models::ActualLRPKey.new(process_guid: process.guid + 'version', index: 0), + actual_lrp_instance_key: ::Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-a'), + state: ::Diego::ActualLRPState::RUNNING, + placement_error: '', + since: two_days_ago_since_epoch_ns + ) + end + let(:actual_lrp_1) do + ::Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: ::Diego::Bbs::Models::ActualLRPKey.new(process_guid: process.guid + 'version', index: 1), + actual_lrp_instance_key: ::Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-b'), + state: ::Diego::ActualLRPState::CLAIMED, + placement_error: '', + since: two_days_ago_since_epoch_ns + (1 * second_in_ns) + ) + end + let(:actual_lrp_2a) do + ::Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: ::Diego::Bbs::Models::ActualLRPKey.new(process_guid: process.guid + 'version', index: 2), + actual_lrp_instance_key: ::Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-c'), + state: ::Diego::ActualLRPState::RUNNING, + placement_error: '', + since: two_days_ago_since_epoch_ns + (2 * second_in_ns) + ) + end + let(:actual_lrp_2b) do + ::Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: ::Diego::Bbs::Models::ActualLRPKey.new(process_guid: process.guid + 'version', index: 2), + actual_lrp_instance_key: ::Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-d'), + state: ::Diego::ActualLRPState::UNCLAIMED, + placement_error: '', + since: two_days_ago_since_epoch_ns + (3 * second_in_ns) + ) + end + + before do + allow(bbs_instances_client).to receive_messages(actual_lrps_by_processes: bbs_actual_lrps_response) + end + + context 'with multiple actual lrps' do + let(:bbs_actual_lrps_response) { [actual_lrp_1, actual_lrp_0, actual_lrp_2a] } # unordered to test sorting by index + + it 'returns all instances sorted by index' do + instances = subject.instances_for_processes([process]) + expect(instances).to eq({ + process.guid => { + 0 => { state: 'RUNNING', since: two_days_in_seconds }, + 1 => { state: 'STARTING', since: two_days_in_seconds - 1 }, + 2 => { state: 'RUNNING', since: two_days_in_seconds - 2 } + } + }) + end + end + + context 'with multiple actual lrps for the same index' do + let(:bbs_actual_lrps_response) { [actual_lrp_0, actual_lrp_1, actual_lrp_2a, actual_lrp_2b] } + + it 'returns the newest instance per index' do + instances = subject.instances_for_processes([process]) + expect(instances).to eq({ + process.guid => { + 0 => { state: 'RUNNING', since: two_days_in_seconds }, + 1 => { state: 'STARTING', since: two_days_in_seconds - 1 }, + 2 => { state: 'STARTING', since: two_days_in_seconds - 3 } + } + }) + end + end + + context 'with number of desired instances being greater than number of actual lrps' do + let(:bbs_actual_lrps_response) { [actual_lrp_0, actual_lrp_1] } + let(:desired_instances) { 4 } + + it 'fills in missing instances as DOWN' do + instances = subject.instances_for_processes([process]) + expect(instances).to eq({ + process.guid => { + 0 => { state: 'RUNNING', since: two_days_in_seconds }, + 1 => { state: 'STARTING', since: two_days_in_seconds - 1 }, + 2 => { state: 'DOWN' }, + 3 => { state: 'DOWN' } + } + }) + end + end + + context 'with multiple processes' do + let(:second_process) { ProcessModel.make } + let(:second_process_actual_lrp_0) do + ::Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: ::Diego::Bbs::Models::ActualLRPKey.new(process_guid: second_process.guid + 'version', index: 0), + actual_lrp_instance_key: ::Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-e'), + state: ::Diego::ActualLRPState::RUNNING, + placement_error: '', + since: two_days_ago_since_epoch_ns + (4 * second_in_ns) + ) + end + let(:second_process_actual_lrp_1) do + ::Diego::Bbs::Models::ActualLRP.new( + actual_lrp_key: ::Diego::Bbs::Models::ActualLRPKey.new(process_guid: second_process.guid + 'version', index: 1), + actual_lrp_instance_key: ::Diego::Bbs::Models::ActualLRPInstanceKey.new(instance_guid: 'instance-f'), + state: ::Diego::ActualLRPState::CRASHED, + placement_error: '', + since: two_days_ago_since_epoch_ns + (5 * second_in_ns) + ) + end + let(:bbs_actual_lrps_response) { [actual_lrp_0, second_process_actual_lrp_0, actual_lrp_1, second_process_actual_lrp_1] } # unordered to test grouping + + it 'returns instances grouped by process guid' do + instances = subject.instances_for_processes([process, second_process]) + expect(instances).to eq({ + process.guid => { + 0 => { state: 'RUNNING', since: two_days_in_seconds }, + 1 => { state: 'STARTING', since: two_days_in_seconds - 1 } + }, + second_process.guid => { + 0 => { state: 'RUNNING', since: two_days_in_seconds - 4 }, + 1 => { state: 'CRASHED', since: two_days_in_seconds - 5 } + } + }) + end + end + + context 'with no actual lrps but desired instances' do + let(:bbs_actual_lrps_response) { [] } + let(:desired_instances) { 2 } + + it 'fills in missing instances as DOWN' do + instances = subject.instances_for_processes([process]) + expect(instances).to eq({ + process.guid => { + 0 => { state: 'DOWN' }, + 1 => { state: 'DOWN' } + } + }) + end + end + + context 'with no actual lrps and no desired instances' do + let(:bbs_actual_lrps_response) { [] } + let(:desired_instances) { 0 } + + it 'returns an empty map for the instances' do + instances = subject.instances_for_processes([process]) + expect(instances).to eq({ + process.guid => {} + }) + end + end + end end end end diff --git a/spec/unit/presenters/v3/process_instances_presenter_spec.rb b/spec/unit/presenters/v3/process_instances_presenter_spec.rb new file mode 100644 index 00000000000..95e7763338a --- /dev/null +++ b/spec/unit/presenters/v3/process_instances_presenter_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' +require 'presenters/v3/process_instances_presenter' + +module VCAP::CloudController::Presenters::V3 + RSpec.describe ProcessInstancesPresenter do + let(:process) { VCAP::CloudController::ProcessModel.make } + let(:instances) do + { + 0 => { state: 'RUNNING', since: 111 }, + 1 => { state: 'STARTING', since: 222 }, + 2 => { state: 'CRASHED', since: 333 } + } + end + + subject(:presenter) { ProcessInstancesPresenter.new(process, instances) } + + describe '#to_hash' do + it 'returns a hash with resources and links' do + result = presenter.to_hash + expect(result).to have_key(:resources) + expect(result).to have_key(:links) + end + + it 'builds instances with correct structure' do + resources = presenter.to_hash[:resources] + expect(resources).to be_an(Array) + expect(resources.length).to eq(3) + + expect(resources[0]).to eq({ index: 0, state: 'RUNNING', since: 111 }) + expect(resources[1]).to eq({ index: 1, state: 'STARTING', since: 222 }) + expect(resources[2]).to eq({ index: 2, state: 'CRASHED', since: 333 }) + end + + it 'builds correct links' do + links = presenter.to_hash[:links] + expect(links[:self][:href]).to eq("#{link_prefix}/v3/processes/#{process.guid}/instances") + expect(links[:process][:href]).to eq("#{link_prefix}/v3/processes/#{process.guid}") + end + + context 'with empty instances' do + let(:instances) { {} } + + it 'returns an empty resources array' do + resources = presenter.to_hash[:resources] + expect(resources).to eq([]) + end + end + end + end +end