Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/controllers/v3/spaces_controller.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 12 additions & 5 deletions app/models/runtime/space.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions app/presenters/v3/space_usage_summary_presenter.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
23 changes: 23 additions & 0 deletions docs/v3/source/includes/api_resources/_spaces.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>
39 changes: 39 additions & 0 deletions docs/v3/source/includes/resources/spaces/_get_usage_summary.md.erb
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions docs/v3/source/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions spec/unit/controllers/v3/spaces_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
67 changes: 67 additions & 0 deletions spec/unit/presenters/v3/space_usage_summary_presenter_spec.rb
Original file line number Diff line number Diff line change
@@ -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