diff --git a/app/controllers/v3/spaces_controller.rb b/app/controllers/v3/spaces_controller.rb index fcecc27d084..deb534ef2cf 100644 --- a/app/controllers/v3/spaces_controller.rb +++ b/app/controllers/v3/spaces_controller.rb @@ -1,5 +1,6 @@ require 'presenters/v3/paginated_list_presenter' require 'presenters/v3/space_presenter' +require 'presenters/v3/space_usage_summary_presenter' require 'messages/space_create_message' require 'messages/space_delete_unmapped_routes_message' require 'messages/space_update_message' @@ -211,6 +212,14 @@ def list_members raise CloudController::Errors::ApiError.new_from_details('UaaUnavailable') end + def show_usage_summary + space = fetch_space(hashed_params[:guid]) + space_not_found! unless space + space_not_found! unless permission_queryer.can_read_from_space?(space.id, space.organization_id) + + render status: :ok, json: Presenters::V3::SpaceUsageSummaryPresenter.new(space) + end + private def fetch_organization(guid) diff --git a/app/models/runtime/space.rb b/app/models/runtime/space.rb index 9706332721e..c587ea1cab0 100644 --- a/app/models/runtime/space.rb +++ b/app/models/runtime/space.rb @@ -269,6 +269,10 @@ def find_visible_service_instance_by_name(name) (shared | source).first end + def number_service_keys + ServiceKey.join(:service_instances, id: :service_instance_id).where(service_instances__space_id: id).count + end + def self.user_visibility_filter(user) { spaces__id: user.space_developer_space_ids. @@ -328,6 +332,14 @@ def members User.dataset.where(id: Role.where(space_id: id).distinct.select(:user_id)) end + def memory_used + started_app_memory + running_task_memory + end + + def running_and_pending_tasks_count + tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count + end + private def has_manager?(user) @@ -339,7 +351,6 @@ def has_auditor?(user) end def memory_remaining - memory_used = started_app_memory + running_task_memory space_quota_definition.memory_limit - memory_used end @@ -363,10 +374,6 @@ def started_app_log_rate_limit processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:log_rate_limit, :instances)) || 0 end - def running_and_pending_tasks_count - tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count - end - def validate_isolation_segment_set(isolation_segment_model) isolation_segment_guids = organization.isolation_segment_models.map(&:guid) return if isolation_segment_guids.include?(isolation_segment_model.guid) diff --git a/app/presenters/v3/space_usage_summary_presenter.rb b/app/presenters/v3/space_usage_summary_presenter.rb new file mode 100644 index 00000000000..821a360c00a --- /dev/null +++ b/app/presenters/v3/space_usage_summary_presenter.rb @@ -0,0 +1,42 @@ +require 'presenters/v3/base_presenter' + +module VCAP::CloudController::Presenters::V3 + class SpaceUsageSummaryPresenter < BasePresenter + def to_hash + { + usage_summary: { + started_instances: started_instances, + memory_in_mb: space.memory_used, + routes: space.routes_dataset.count, + service_instances: space.service_instances_dataset.count, + reserved_ports: VCAP::CloudController::SpaceReservedRoutePorts.new(space).count, + domains: space.organization.owned_private_domains_dataset.count, + per_app_tasks: space.running_and_pending_tasks_count, + service_keys: space.number_service_keys + }, + links: build_links + } + end + + private + + def space + @resource + end + + def started_instances + space.processes_dataset.where(state: VCAP::CloudController::ProcessModel::STARTED).sum(:instances) || 0 + end + + def build_links + { + self: { + href: url_builder.build_url(path: "/v3/spaces/#{space.guid}/usage_summary") + }, + space: { + href: url_builder.build_url(path: "/v3/spaces/#{space.guid}") + } + } + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 5994216e73b..ccdd5788eaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -276,6 +276,7 @@ patch '/spaces/:guid', to: 'spaces_v3#update' delete 'spaces/:guid', to: 'spaces_v3#destroy' delete 'spaces/:guid/routes', to: 'spaces_v3#delete_unmapped_routes' + get '/spaces/:guid/usage_summary', to: 'spaces_v3#show_usage_summary' get '/spaces/:guid/relationships/isolation_segment', to: 'spaces_v3#show_isolation_segment' patch '/spaces/:guid/relationships/isolation_segment', to: 'spaces_v3#update_isolation_segment' get '/spaces/:guid/users', to: 'spaces_v3#list_members' diff --git a/docs/v3/source/includes/api_resources/_spaces.erb b/docs/v3/source/includes/api_resources/_spaces.erb index d2d284b8e85..e9a24461bf2 100644 --- a/docs/v3/source/includes/api_resources/_spaces.erb +++ b/docs/v3/source/includes/api_resources/_spaces.erb @@ -145,3 +145,26 @@ ] } <% end %> + +<% content_for :space_usage_summary do %> +{ + "usage_summary": { + "started_instances": 3, + "memory_in_mb": 3072, + "routes": 3, + "service_instances": 2, + "reserved_ports": 1, + "domains": 1, + "per_app_tasks": 0, + "service_keys": 1 + }, + "links": { + "self": { + "href": "https://api.example.org/v3/spaces/f47ac10b-58cc-4372-a567-0e02b2c3d479/usage_summary" + }, + "organization": { + "href": "https://api.example.org/v3/spaces/f47ac10b-58cc-4372-a567-0e02b2c3d479" + } + } +} +<% end %> diff --git a/docs/v3/source/includes/resources/spaces/_get_usage_summary.md.erb b/docs/v3/source/includes/resources/spaces/_get_usage_summary.md.erb new file mode 100644 index 00000000000..d4b56e8176f --- /dev/null +++ b/docs/v3/source/includes/resources/spaces/_get_usage_summary.md.erb @@ -0,0 +1,39 @@ +### Get space usage summary + +``` +Example Request +``` + +```shell +curl "https://api.example.org/v3/spaces/[guid]/usage_summary" \ + -X GET \ + -H "Authorization: bearer [token]" +``` + +``` +Example Response +``` + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +<%= yield_content :space_usage_summary, '/v3/spaces/:guid/usage_summary' %> +``` + +This endpoint retrieves a usage summary for the specified space. It provides aggregated data about the space's resource usage, such as memory, routes and services. + +#### Definition +`GET /v3/spaces/:guid/usage_summary` + +#### Permitted roles +| +--- | +Admin | +Admin Read-Only | +Global Auditor | +Org Manager | +Space Auditor | +Space Developer | +Space Manager | +Space Supporter | diff --git a/docs/v3/source/includes/upgrade_guide/conceptual_changes/_resource_summaries.md b/docs/v3/source/includes/upgrade_guide/conceptual_changes/_resource_summaries.md index 000fbe54166..1d88f49e547 100644 --- a/docs/v3/source/includes/upgrade_guide/conceptual_changes/_resource_summaries.md +++ b/docs/v3/source/includes/upgrade_guide/conceptual_changes/_resource_summaries.md @@ -71,5 +71,5 @@ in the response body. #### Usage summary endpoints There are still a couple of endpoints in V3 that provide a basic summary of -instance and memory usage. See the [org summary](#get-usage-summary) and +instance and memory usage. See the [org summary](#get-usage-summary), [space summary](#get-space-usage-summary) and [platform summary](#get-platform-usage-summary) endpoints. diff --git a/docs/v3/source/index.html.md b/docs/v3/source/index.html.md index 03c63aecaca..c007e7fc7de 100644 --- a/docs/v3/source/index.html.md +++ b/docs/v3/source/index.html.md @@ -354,6 +354,7 @@ includes: - resources/spaces/update - resources/spaces/delete - resources/spaces/get_assigned_isolation_segment + - resources/spaces/get_usage_summary - resources/spaces/manage_isolation_segment - resources/spaces/list_users - resources/space_features/header diff --git a/spec/unit/controllers/v3/spaces_controller_spec.rb b/spec/unit/controllers/v3/spaces_controller_spec.rb index e3a432256e3..f11be5665d4 100644 --- a/spec/unit/controllers/v3/spaces_controller_spec.rb +++ b/spec/unit/controllers/v3/spaces_controller_spec.rb @@ -952,4 +952,34 @@ end end end + + describe '#show_usage_summary' do + let(:user) { set_current_user(VCAP::CloudController::User.make) } + + let!(:org) { VCAP::CloudController::Organization.make(name: 'Lyle\'s Farm') } + let!(:space) { VCAP::CloudController::Space.make(name: 'Chicken', organization: org) } + + context 'when the user has permissions to read from the space' do + before { allow_user_read_access_for(user, orgs: [org], spaces: [space]) } + + it 'succeeds' do + get :show_usage_summary, params: { guid: space.guid } + + expect(response).to have_http_status(:ok) + expect(response.body).to include 'usage_summary' + end + end + + context 'when the user does not have permissions to read from the space' do + before { allow_user_read_access_for(user, orgs: [], spaces: []) } + + it 'throws ResourceNotFound error' do + get :show_usage_summary, params: { guid: space.guid } + + expect(response).to have_http_status(:not_found) + expect(response.body).to include 'ResourceNotFound' + expect(response.body).to include 'Space not found' + end + end + end end diff --git a/spec/unit/presenters/v3/space_usage_summary_presenter_spec.rb b/spec/unit/presenters/v3/space_usage_summary_presenter_spec.rb new file mode 100644 index 00000000000..b36ab1cb09b --- /dev/null +++ b/spec/unit/presenters/v3/space_usage_summary_presenter_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require 'presenters/v3/space_usage_summary_presenter' + +module VCAP::CloudController::Presenters::V3 + RSpec.describe SpaceUsageSummaryPresenter do + let(:org) { VCAP::CloudController::Organization.make } + let(:space) { VCAP::CloudController::Space.make(organization: org) } + + context 'empty space' do + describe '#to_hash' do + let(:result) { SpaceUsageSummaryPresenter.new(space).to_hash } + + it 'presents the space usage summary as json' do + expect(result[:usage_summary][:started_instances]).to eq(0) + expect(result[:usage_summary][:memory_in_mb]).to eq(0) + expect(result[:usage_summary][:routes]).to eq(0) + expect(result[:usage_summary][:service_instances]).to eq(0) + expect(result[:usage_summary][:reserved_ports]).to eq(0) + expect(result[:usage_summary][:domains]).to eq(0) + expect(result[:usage_summary][:per_app_tasks]).to eq(0) + expect(result[:usage_summary][:service_keys]).to eq(0) + + expect(result[:links][:self][:href]).to match(%r{/v3/spaces/#{space.guid}/usage_summary$}) + expect(result[:links][:space][:href]).to match(%r{/v3/spaces/#{space.guid}$}) + end + end + end + + context 'space with instances, routes and services' do + before do + router_group = double('router_group', type: 'tcp', reservable_ports: [4444]) + routing_api_client = double('routing_api_client', router_group: router_group, enabled?: true) + allow(CloudController::DependencyLocator).to receive(:instance).and_return(double(:api_client, routing_api_client:)) + end + + let(:app_model) { VCAP::CloudController::AppModel.make(name: 'App Model', space: space) } + let!(:process) { VCAP::CloudController::ProcessModel.make(:process, state: VCAP::CloudController::ProcessModel::STARTED, memory: 512, app: app_model) } + let!(:task) { VCAP::CloudController::TaskModel.make(app: app_model, state: VCAP::CloudController::TaskModel::RUNNING_STATE, memory_in_mb: 512) } + let(:shared_domain) { VCAP::CloudController::SharedDomain.make(router_group_guid: '123') } + let!(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) } + let!(:route) { VCAP::CloudController::Route.make(host: '', domain: shared_domain, space: space, port: 4444) } + let(:broker) { VCAP::CloudController::ServiceBroker.make } + let(:service) { VCAP::CloudController::Service.make(service_broker: broker) } + let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service, public: true) } + let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space:, service_plan:) } + let!(:service_key) { VCAP::CloudController::ServiceKey.make(service_instance:) } + + describe '#to_hash' do + let(:result) { SpaceUsageSummaryPresenter.new(space).to_hash } + + it 'presents the space usage summary as json' do + expect(result[:usage_summary][:started_instances]).to eq(1) + expect(result[:usage_summary][:memory_in_mb]).to eq(1024) + expect(result[:usage_summary][:routes]).to eq(1) + expect(result[:usage_summary][:service_instances]).to eq(1) + expect(result[:usage_summary][:reserved_ports]).to eq(1) + expect(result[:usage_summary][:domains]).to eq(1) + expect(result[:usage_summary][:per_app_tasks]).to eq(1) + expect(result[:usage_summary][:service_keys]).to eq(1) + + expect(result[:links][:self][:href]).to match(%r{/v3/spaces/#{space.guid}/usage_summary$}) + expect(result[:links][:space][:href]).to match(%r{/v3/spaces/#{space.guid}$}) + end + end + end + end +end