Skip to content

Commit f9e7faf

Browse files
authored
Implement "/v3/spaces/#{space.guid}/usage_summary" endpoint (#4468)
* Implement /v3/spaces/#{space.guid}/usage_summary * Fix docu anchors * Optimise and fix queries, fix test description * Add "space_usage_summary" block to spaces docu
1 parent ca28383 commit f9e7faf

File tree

10 files changed

+225
-6
lines changed

10 files changed

+225
-6
lines changed

app/controllers/v3/spaces_controller.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'presenters/v3/paginated_list_presenter'
22
require 'presenters/v3/space_presenter'
3+
require 'presenters/v3/space_usage_summary_presenter'
34
require 'messages/space_create_message'
45
require 'messages/space_delete_unmapped_routes_message'
56
require 'messages/space_update_message'
@@ -211,6 +212,14 @@ def list_members
211212
raise CloudController::Errors::ApiError.new_from_details('UaaUnavailable')
212213
end
213214

215+
def show_usage_summary
216+
space = fetch_space(hashed_params[:guid])
217+
space_not_found! unless space
218+
space_not_found! unless permission_queryer.can_read_from_space?(space.id, space.organization_id)
219+
220+
render status: :ok, json: Presenters::V3::SpaceUsageSummaryPresenter.new(space)
221+
end
222+
214223
private
215224

216225
def fetch_organization(guid)

app/models/runtime/space.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ def find_visible_service_instance_by_name(name)
269269
(shared | source).first
270270
end
271271

272+
def number_service_keys
273+
ServiceKey.join(:service_instances, id: :service_instance_id).where(service_instances__space_id: id).count
274+
end
275+
272276
def self.user_visibility_filter(user)
273277
{
274278
spaces__id: user.space_developer_space_ids.
@@ -328,6 +332,14 @@ def members
328332
User.dataset.where(id: Role.where(space_id: id).distinct.select(:user_id))
329333
end
330334

335+
def memory_used
336+
started_app_memory + running_task_memory
337+
end
338+
339+
def running_and_pending_tasks_count
340+
tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count
341+
end
342+
331343
private
332344

333345
def has_manager?(user)
@@ -339,7 +351,6 @@ def has_auditor?(user)
339351
end
340352

341353
def memory_remaining
342-
memory_used = started_app_memory + running_task_memory
343354
space_quota_definition.memory_limit - memory_used
344355
end
345356

@@ -363,10 +374,6 @@ def started_app_log_rate_limit
363374
processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:log_rate_limit, :instances)) || 0
364375
end
365376

366-
def running_and_pending_tasks_count
367-
tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count
368-
end
369-
370377
def validate_isolation_segment_set(isolation_segment_model)
371378
isolation_segment_guids = organization.isolation_segment_models.map(&:guid)
372379
return if isolation_segment_guids.include?(isolation_segment_model.guid)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'presenters/v3/base_presenter'
2+
3+
module VCAP::CloudController::Presenters::V3
4+
class SpaceUsageSummaryPresenter < BasePresenter
5+
def to_hash
6+
{
7+
usage_summary: {
8+
started_instances: started_instances,
9+
memory_in_mb: space.memory_used,
10+
routes: space.routes_dataset.count,
11+
service_instances: space.service_instances_dataset.count,
12+
reserved_ports: VCAP::CloudController::SpaceReservedRoutePorts.new(space).count,
13+
domains: space.organization.owned_private_domains_dataset.count,
14+
per_app_tasks: space.running_and_pending_tasks_count,
15+
service_keys: space.number_service_keys
16+
},
17+
links: build_links
18+
}
19+
end
20+
21+
private
22+
23+
def space
24+
@resource
25+
end
26+
27+
def started_instances
28+
space.processes_dataset.where(state: VCAP::CloudController::ProcessModel::STARTED).sum(:instances) || 0
29+
end
30+
31+
def build_links
32+
{
33+
self: {
34+
href: url_builder.build_url(path: "/v3/spaces/#{space.guid}/usage_summary")
35+
},
36+
space: {
37+
href: url_builder.build_url(path: "/v3/spaces/#{space.guid}")
38+
}
39+
}
40+
end
41+
end
42+
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@
276276
patch '/spaces/:guid', to: 'spaces_v3#update'
277277
delete 'spaces/:guid', to: 'spaces_v3#destroy'
278278
delete 'spaces/:guid/routes', to: 'spaces_v3#delete_unmapped_routes'
279+
get '/spaces/:guid/usage_summary', to: 'spaces_v3#show_usage_summary'
279280
get '/spaces/:guid/relationships/isolation_segment', to: 'spaces_v3#show_isolation_segment'
280281
patch '/spaces/:guid/relationships/isolation_segment', to: 'spaces_v3#update_isolation_segment'
281282
get '/spaces/:guid/users', to: 'spaces_v3#list_members'

docs/v3/source/includes/api_resources/_spaces.erb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,26 @@
145145
]
146146
}
147147
<% end %>
148+
149+
<% content_for :space_usage_summary do %>
150+
{
151+
"usage_summary": {
152+
"started_instances": 3,
153+
"memory_in_mb": 3072,
154+
"routes": 3,
155+
"service_instances": 2,
156+
"reserved_ports": 1,
157+
"domains": 1,
158+
"per_app_tasks": 0,
159+
"service_keys": 1
160+
},
161+
"links": {
162+
"self": {
163+
"href": "https://api.example.org/v3/spaces/f47ac10b-58cc-4372-a567-0e02b2c3d479/usage_summary"
164+
},
165+
"organization": {
166+
"href": "https://api.example.org/v3/spaces/f47ac10b-58cc-4372-a567-0e02b2c3d479"
167+
}
168+
}
169+
}
170+
<% end %>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
### Get space usage summary
2+
3+
```
4+
Example Request
5+
```
6+
7+
```shell
8+
curl "https://api.example.org/v3/spaces/[guid]/usage_summary" \
9+
-X GET \
10+
-H "Authorization: bearer [token]"
11+
```
12+
13+
```
14+
Example Response
15+
```
16+
17+
```http
18+
HTTP/1.1 200 OK
19+
Content-Type: application/json
20+
21+
<%= yield_content :space_usage_summary, '/v3/spaces/:guid/usage_summary' %>
22+
```
23+
24+
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.
25+
26+
#### Definition
27+
`GET /v3/spaces/:guid/usage_summary`
28+
29+
#### Permitted roles
30+
|
31+
--- |
32+
Admin |
33+
Admin Read-Only |
34+
Global Auditor |
35+
Org Manager |
36+
Space Auditor |
37+
Space Developer |
38+
Space Manager |
39+
Space Supporter |

docs/v3/source/includes/upgrade_guide/conceptual_changes/_resource_summaries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,5 @@ in the response body.
7171
#### Usage summary endpoints
7272

7373
There are still a couple of endpoints in V3 that provide a basic summary of
74-
instance and memory usage. See the [org summary](#get-usage-summary) and
74+
instance and memory usage. See the [org summary](#get-usage-summary), [space summary](#get-space-usage-summary) and
7575
[platform summary](#get-platform-usage-summary) endpoints.

docs/v3/source/index.html.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ includes:
354354
- resources/spaces/update
355355
- resources/spaces/delete
356356
- resources/spaces/get_assigned_isolation_segment
357+
- resources/spaces/get_usage_summary
357358
- resources/spaces/manage_isolation_segment
358359
- resources/spaces/list_users
359360
- resources/space_features/header

spec/unit/controllers/v3/spaces_controller_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,4 +952,34 @@
952952
end
953953
end
954954
end
955+
956+
describe '#show_usage_summary' do
957+
let(:user) { set_current_user(VCAP::CloudController::User.make) }
958+
959+
let!(:org) { VCAP::CloudController::Organization.make(name: 'Lyle\'s Farm') }
960+
let!(:space) { VCAP::CloudController::Space.make(name: 'Chicken', organization: org) }
961+
962+
context 'when the user has permissions to read from the space' do
963+
before { allow_user_read_access_for(user, orgs: [org], spaces: [space]) }
964+
965+
it 'succeeds' do
966+
get :show_usage_summary, params: { guid: space.guid }
967+
968+
expect(response).to have_http_status(:ok)
969+
expect(response.body).to include 'usage_summary'
970+
end
971+
end
972+
973+
context 'when the user does not have permissions to read from the space' do
974+
before { allow_user_read_access_for(user, orgs: [], spaces: []) }
975+
976+
it 'throws ResourceNotFound error' do
977+
get :show_usage_summary, params: { guid: space.guid }
978+
979+
expect(response).to have_http_status(:not_found)
980+
expect(response.body).to include 'ResourceNotFound'
981+
expect(response.body).to include 'Space not found'
982+
end
983+
end
984+
end
955985
end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
require 'spec_helper'
2+
require 'presenters/v3/space_usage_summary_presenter'
3+
4+
module VCAP::CloudController::Presenters::V3
5+
RSpec.describe SpaceUsageSummaryPresenter do
6+
let(:org) { VCAP::CloudController::Organization.make }
7+
let(:space) { VCAP::CloudController::Space.make(organization: org) }
8+
9+
context 'empty space' do
10+
describe '#to_hash' do
11+
let(:result) { SpaceUsageSummaryPresenter.new(space).to_hash }
12+
13+
it 'presents the space usage summary as json' do
14+
expect(result[:usage_summary][:started_instances]).to eq(0)
15+
expect(result[:usage_summary][:memory_in_mb]).to eq(0)
16+
expect(result[:usage_summary][:routes]).to eq(0)
17+
expect(result[:usage_summary][:service_instances]).to eq(0)
18+
expect(result[:usage_summary][:reserved_ports]).to eq(0)
19+
expect(result[:usage_summary][:domains]).to eq(0)
20+
expect(result[:usage_summary][:per_app_tasks]).to eq(0)
21+
expect(result[:usage_summary][:service_keys]).to eq(0)
22+
23+
expect(result[:links][:self][:href]).to match(%r{/v3/spaces/#{space.guid}/usage_summary$})
24+
expect(result[:links][:space][:href]).to match(%r{/v3/spaces/#{space.guid}$})
25+
end
26+
end
27+
end
28+
29+
context 'space with instances, routes and services' do
30+
before do
31+
router_group = double('router_group', type: 'tcp', reservable_ports: [4444])
32+
routing_api_client = double('routing_api_client', router_group: router_group, enabled?: true)
33+
allow(CloudController::DependencyLocator).to receive(:instance).and_return(double(:api_client, routing_api_client:))
34+
end
35+
36+
let(:app_model) { VCAP::CloudController::AppModel.make(name: 'App Model', space: space) }
37+
let!(:process) { VCAP::CloudController::ProcessModel.make(:process, state: VCAP::CloudController::ProcessModel::STARTED, memory: 512, app: app_model) }
38+
let!(:task) { VCAP::CloudController::TaskModel.make(app: app_model, state: VCAP::CloudController::TaskModel::RUNNING_STATE, memory_in_mb: 512) }
39+
let(:shared_domain) { VCAP::CloudController::SharedDomain.make(router_group_guid: '123') }
40+
let!(:private_domain) { VCAP::CloudController::PrivateDomain.make(owning_organization: org) }
41+
let!(:route) { VCAP::CloudController::Route.make(host: '', domain: shared_domain, space: space, port: 4444) }
42+
let(:broker) { VCAP::CloudController::ServiceBroker.make }
43+
let(:service) { VCAP::CloudController::Service.make(service_broker: broker) }
44+
let(:service_plan) { VCAP::CloudController::ServicePlan.make(service: service, public: true) }
45+
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space:, service_plan:) }
46+
let!(:service_key) { VCAP::CloudController::ServiceKey.make(service_instance:) }
47+
48+
describe '#to_hash' do
49+
let(:result) { SpaceUsageSummaryPresenter.new(space).to_hash }
50+
51+
it 'presents the space usage summary as json' do
52+
expect(result[:usage_summary][:started_instances]).to eq(1)
53+
expect(result[:usage_summary][:memory_in_mb]).to eq(1024)
54+
expect(result[:usage_summary][:routes]).to eq(1)
55+
expect(result[:usage_summary][:service_instances]).to eq(1)
56+
expect(result[:usage_summary][:reserved_ports]).to eq(1)
57+
expect(result[:usage_summary][:domains]).to eq(1)
58+
expect(result[:usage_summary][:per_app_tasks]).to eq(1)
59+
expect(result[:usage_summary][:service_keys]).to eq(1)
60+
61+
expect(result[:links][:self][:href]).to match(%r{/v3/spaces/#{space.guid}/usage_summary$})
62+
expect(result[:links][:space][:href]).to match(%r{/v3/spaces/#{space.guid}$})
63+
end
64+
end
65+
end
66+
end
67+
end

0 commit comments

Comments
 (0)