Skip to content

Commit 76fe600

Browse files
committed
Implement embed=process_instances
Process instances can be embedded into process resources; the parameter embed=process_instances is available for the following API endpoints: - GET /v3/processes/:guid?embed=process_instances - GET /v3/apps/:guid/processes/:type?embed=process_instances - GET /v3/processes?embed=process_instances - GET /v3/apps/:guid/processes?embed=process_instances A new ProcessShowMessage has been introduced; ProcessesListMessage has been extended. Both messages allow the 'embed' key that is checked with the EmbedParamValidator. The process output is enhanced by the EmbedProcessInstancesDecorator that fetches the required information from Diego and adds a field 'process_instances' to the resource. Process resources now also contain a link to 'process_instances'. API docs have been extended with the new process instances pseudo-resource and a description of the 'embed' parameter.
1 parent 1290b15 commit 76fe600

20 files changed

+685
-17
lines changed

app/controllers/v3/processes_controller.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'presenters/v3/process_presenter'
33
require 'presenters/v3/process_stats_presenter'
44
require 'presenters/v3/process_instances_presenter'
5+
require 'decorators/embed_process_instances_decorator'
56
require 'cloud_controller/paging/pagination_options'
67
require 'actions/process_delete'
78
require 'fetchers/process_list_fetcher'
@@ -10,6 +11,7 @@
1011
require 'actions/process_terminate'
1112
require 'actions/process_update'
1213
require 'messages/process_scale_message'
14+
require 'messages/process_show_message'
1315
require 'messages/process_update_message'
1416
require 'messages/processes_list_message'
1517
require 'controllers/v3/mixins/app_sub_resource'
@@ -41,17 +43,30 @@ def index
4143
end
4244
end
4345

46+
decorators = []
47+
decorators << EmbedProcessInstancesDecorator if EmbedProcessInstancesDecorator.match?(message.embed)
48+
4449
render status: :ok, json: Presenters::V3::PaginatedListPresenter.new(
4550
presenter: Presenters::V3::ProcessPresenter,
4651
paginated_result: SequelPaginator.new.get_page(dataset, message.try(:pagination_options)),
4752
path: base_url(resource: 'processes'),
48-
message: message
53+
message: message,
54+
decorators: decorators
4955
)
5056
end
5157

5258
def show
53-
# TODO
54-
render status: :ok, json: Presenters::V3::ProcessPresenter.new(@process, show_secrets: permission_queryer.can_read_secrets_in_space?(@space.id, @space.organization_id))
59+
message = ProcessShowMessage.from_params(query_params)
60+
invalid_param!(message.errors.full_messages) unless message.valid?
61+
62+
decorators = []
63+
decorators << EmbedProcessInstancesDecorator if EmbedProcessInstancesDecorator.match?(message.embed)
64+
65+
render status: :ok, json: Presenters::V3::ProcessPresenter.new(
66+
@process,
67+
show_secrets: permission_queryer.can_read_secrets_in_space?(@space.id, @space.organization_id),
68+
decorators: decorators
69+
)
5570
end
5671

5772
def update
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module VCAP::CloudController
2+
class EmbedProcessInstancesDecorator
3+
class << self
4+
def match?(embed)
5+
embed&.include?('process_instances')
6+
end
7+
8+
def decorate(hash, processes)
9+
instances_reporters = CloudController::DependencyLocator.instance.instances_reporters
10+
instances = instances_reporters.instances_for_processes(processes)
11+
12+
if hash.key?(:resources)
13+
# Decorate PaginatedListPresenter
14+
processes.each do |process|
15+
resource_index = hash[:resources].find_index { |resource| resource[:guid] == process.guid }
16+
next unless resource_index # Should not happen...
17+
18+
hash[:resources][resource_index] = embed_process_instances(hash[:resources][resource_index], process_instances(instances, process.guid))
19+
end
20+
else
21+
# Decorate ProcessPresenter
22+
hash = embed_process_instances(hash, process_instances(instances, hash[:guid]))
23+
end
24+
25+
hash
26+
end
27+
28+
private
29+
30+
def process_instances(instances, process_guid)
31+
instances[process_guid]&.map do |index, instance|
32+
{
33+
index: index,
34+
state: instance[:state],
35+
since: instance[:since]
36+
}
37+
end || []
38+
end
39+
40+
def embed_process_instances(resource_hash, process_instances)
41+
hash_as_array = resource_hash.to_a
42+
before_relationships = hash_as_array.index { |k, _| k == :relationships } || hash_as_array.length
43+
hash_as_array.insert(before_relationships, [:process_instances, process_instances])
44+
hash_as_array.to_h
45+
end
46+
end
47+
end
48+
end

app/messages/base_message.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,22 @@ def validate(record)
139139
end
140140
end
141141

142+
class EmbedParamValidator < ActiveModel::Validator
143+
def validate(record)
144+
return unless record.requested?(:embed)
145+
146+
key_counts = Hash.new(0)
147+
record.embed.each do |embed_candidate|
148+
if options[:valid_values].member?(embed_candidate)
149+
key_counts[embed_candidate] += 1
150+
record.errors.add(:base, message: "Duplicate embedded resource: '#{embed_candidate}'") if key_counts[embed_candidate] == 2
151+
else
152+
record.errors.add(:base, message: "Invalid embedded resource: '#{embed_candidate}'. Valid embedded resources are: '#{options[:valid_values].join("', '")}'")
153+
end
154+
end
155+
end
156+
end
157+
142158
class LifecycleTypeParamValidator < ActiveModel::Validator
143159
def validate(record)
144160
return unless record.requested?(:lifecycle_type)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
require 'messages/base_message'
2+
3+
module VCAP::CloudController
4+
class ProcessShowMessage < BaseMessage
5+
register_allowed_keys [:embed]
6+
7+
validates_with NoAdditionalParamsValidator
8+
validates_with EmbedParamValidator, valid_values: ['process_instances']
9+
10+
def self.from_params(params)
11+
super(params, %w[embed])
12+
end
13+
end
14+
end

app/messages/processes_list_message.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ class ProcessesListMessage < MetadataListMessage
88
space_guids
99
organization_guids
1010
app_guids
11+
embed
1112
]
1213

1314
validates_with NoAdditionalParamsValidator # from BaseMessage
15+
validates_with EmbedParamValidator, valid_values: ['process_instances']
1416

17+
validates :space_guids, array: true, allow_nil: true
18+
validates :organization_guids, array: true, allow_nil: true
1519
validates :app_guids, array: true, allow_nil: true
1620
validate :app_nested_request, if: -> { app_guid.present? }
1721

1822
def self.from_params(params)
19-
super(params, %w[types space_guids organization_guids app_guids])
23+
super(params, %w[types space_guids organization_guids app_guids embed])
2024
end
2125

2226
def to_param_hash

app/presenters/v3/process_presenter.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def to_hash
1414

1515
readiness_health_check_data = { invocation_timeout: process.readiness_health_check_invocation_timeout, interval: process.readiness_health_check_interval }
1616
readiness_health_check_data[:endpoint] = process.readiness_health_check_http_endpoint if process.readiness_health_check_type == HealthCheckTypes::HTTP
17-
{
17+
18+
hash = {
1819
guid: process.guid,
1920
created_at: process.created_at,
2021
updated_at: process.updated_at,
@@ -44,6 +45,8 @@ def to_hash
4445
},
4546
links: build_links
4647
}
48+
49+
@decorators.reduce(hash) { |memo, d| d.decorate(memo, [process]) }
4750
end
4851

4952
private
@@ -66,7 +69,8 @@ def build_links
6669
scale: { href: url_builder.build_url(path: "/v3/processes/#{process.guid}/actions/scale"), method: 'POST' },
6770
app: { href: url_builder.build_url(path: "/v3/apps/#{process.app_guid}") },
6871
space: { href: url_builder.build_url(path: "/v3/spaces/#{process.space_guid}") },
69-
stats: { href: url_builder.build_url(path: "/v3/processes/#{process.guid}/stats") }
72+
stats: { href: url_builder.build_url(path: "/v3/processes/#{process.guid}/stats") },
73+
process_instances: { href: url_builder.build_url(path: "/v3/processes/#{process.guid}/process_instances") }
7074
}
7175
end
7276
end

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
},
5656
"stats": {
5757
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/stats"
58+
},
59+
"process_instances": {
60+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/process_instances"
5861
}
5962
}
6063
}
@@ -136,6 +139,9 @@
136139
},
137140
"stats": {
138141
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/stats"
142+
},
143+
"process_instances": {
144+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/process_instances"
139145
}
140146
}
141147
},
@@ -194,6 +200,9 @@
194200
},
195201
"stats": {
196202
"href": "https://api.example.org/v3/processes/3fccacd9-4b02-4b96-8d02-8e865865e9eb/stats"
203+
},
204+
"process_instances": {
205+
"href": "https://api.example.org/v3/processes/3fccacd9-4b02-4b96-8d02-8e865865e9eb/process_instances"
197206
}
198207
}
199208
}
@@ -311,3 +320,123 @@
311320
"details": null
312321
}
313322
<% end %>
323+
324+
<% content_for :single_process_instance do %>
325+
{
326+
"index": 0,
327+
"state": "RUNNING",
328+
"since": 123456789
329+
}
330+
<% end %>
331+
332+
<% content_for :process_instances do %>
333+
{
334+
"resources": [
335+
{
336+
"index": 0,
337+
"state": "RUNNING",
338+
"since": 123456789
339+
},
340+
{
341+
"index": 1,
342+
"state": "STARTING",
343+
"since": 123
344+
},
345+
{
346+
"index": 2,
347+
"state": "DOWN",
348+
"since": 456
349+
}
350+
],
351+
"links": {
352+
"self": {
353+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/process_instances"
354+
},
355+
"process": {
356+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82"
357+
}
358+
}
359+
}
360+
<% end %>
361+
362+
<% content_for :single_process_with_embedded_process_instances do %>
363+
{
364+
"guid": "6a901b7c-9417-4dc1-8189-d3234aa0ab82",
365+
"type": "web",
366+
"command": "rackup",
367+
"user": "vcap",
368+
"instances": 5,
369+
"memory_in_mb": 256,
370+
"disk_in_mb": 1024,
371+
"log_rate_limit_in_bytes_per_second": 1024,
372+
"health_check": {
373+
"type": "port",
374+
"data": {
375+
"timeout": null
376+
}
377+
},
378+
"readiness_health_check": {
379+
"type": "process",
380+
"data": {
381+
"invocation_timeout": null
382+
}
383+
},
384+
"process_instances": [
385+
{
386+
"index": 0,
387+
"state": "RUNNING",
388+
"since": 123456789
389+
},
390+
{
391+
"index": 1,
392+
"state": "STARTING",
393+
"since": 123
394+
},
395+
{
396+
"index": 2,
397+
"state": "DOWN",
398+
"since": 456
399+
}
400+
],
401+
"relationships": {
402+
"app": {
403+
"data": {
404+
"guid": "ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5"
405+
}
406+
},
407+
"revision": {
408+
"data": {
409+
"guid": "885735b5-aea4-4cf5-8e44-961af0e41920"
410+
}
411+
}
412+
},
413+
"metadata": {
414+
"labels": {},
415+
"annotations": {}
416+
},
417+
"created_at": "2016-03-23T18:48:22Z",
418+
"updated_at": "2016-03-23T18:48:42Z",
419+
"version": "e9df685c-0464-4aa7-b5f0-8ed843077c13",
420+
"links": {
421+
"self": {
422+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82"
423+
},
424+
"scale": {
425+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/actions/scale",
426+
"method": "POST"
427+
},
428+
"app": {
429+
"href": "https://api.example.org/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5"
430+
},
431+
"space": {
432+
"href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576"
433+
},
434+
"stats": {
435+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/stats"
436+
},
437+
"process_instances": {
438+
"href": "https://api.example.org/v3/processes/6a901b7c-9417-4dc1-8189-d3234aa0ab82/process_instances"
439+
}
440+
}
441+
}
442+
<% end %>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Embed
2+
3+
The `embed` parameter allows clients to fetch resources and include information of related pseudo-resources directly in the response. This is different from the [`include`](#include) parameter, which is used for fetching parent resources.
4+
5+
**Important:** The `embed` feature is specifically designed for pseudo-resources that, by design, do not have their own list endpoint. These are resources that are tightly coupled to their parent resource and only make sense in the context of that parent. While they may have a GET endpoint to retrieve them individually for a specific parent, they cannot be listed independently across all parents.
6+
7+
For example, `process_instances` is a pseudo-resource that provides a lightweight view of process instance states. It can be embedded into process resources to avoid making additional API calls, but you cannot list all process instances across all processes in the system.
8+
9+
Developers may choose to use the `embed` feature to reduce the number of API calls. The embed query param can be used with a single resource or a list of resources.
10+
11+
### Resources with embed
12+
13+
The following resources can take an `embed` parameter:
14+
15+
Resource | Allowed values
16+
-------- | --------------
17+
**processes** | `process_instances`
18+
**processes/[:guid]** | `process_instances`
19+
**apps/[:guid]/processes** | `process_instances`
20+
**apps/[:guid]/processes/[:type]** | `process_instances`

docs/v3/source/includes/resources/processes/_get.md.erb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,33 @@ Content-Type: application/json
2525
`GET /v3/processes/:guid` <br>
2626
`GET /v3/apps/:guid/processes/:type`
2727

28+
#### Query parameters
29+
30+
Name | Type | Description
31+
---- | ---- | ------------
32+
**embed** | _string_ | Comma-delimited list of resources to embed in the response. Valid values are `process_instances`. See [embed](#embed) for more details.
33+
34+
```
35+
Example Request with Embedded Process Instances
36+
```
37+
38+
```shell
39+
curl "https://api.example.org/v3/processes/[guid]?embed=process_instances" \
40+
-X GET \
41+
-H "Authorization: bearer [token]"
42+
```
43+
44+
```
45+
Example Response with Embedded Process Instances
46+
```
47+
48+
```http
49+
HTTP/1.1 200 OK
50+
Content-Type: application/json
51+
52+
<%= yield_content :single_process_with_embedded_process_instances %>
53+
```
54+
2855
#### Permitted roles
2956
Role | Notes |
3057
--- | ---

0 commit comments

Comments
 (0)