Skip to content
Open
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
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ Metrics/ClassLength:
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/action/actions.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/workflow/workflow_executor_proxy.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/services/smart_action_checker.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/frontend_validation_utils.rb'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def self.routes
{ name: 'associate_related', handler: -> { Resources::Related::AssociateRelated.new.routes } },
{ name: 'dissociate_related', handler: -> { Resources::Related::DissociateRelated.new.routes } },
{ name: 'update_related', handler: -> { Resources::Related::UpdateRelated.new.routes } },
{ name: 'update_field', handler: -> { Resources::UpdateField.new.routes } }
{ name: 'update_field', handler: -> { Resources::UpdateField.new.routes } },
{ name: 'workflow_executor_proxy', handler: -> { Workflow::WorkflowExecutorProxy.new.routes } }
]

all_routes = {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
require 'faraday'

module ForestAdminAgent
module Routes
module Workflow
# Forwards workflow-execution traffic from the agent to the workflow executor.
# Mounted only when the integrator sets `workflow_executor_url`
class WorkflowExecutorProxy < AbstractAuthenticatedRoute
AGENT_PREFIX = '/_internal/workflow-executions'.freeze
EXECUTOR_PREFIX = '/runs'.freeze
FORWARDED_HEADERS = %w[Authorization Cookie].freeze
ROUTING_KEYS = %w[run_id route_alias controller action format].freeze
OPEN_TIMEOUT = 2
GET_TIMEOUT = 10
TRIGGER_TIMEOUT = 120

def setup_routes
return self unless executor_configured?

add_route(
'forest_workflow_run_show',
'get',
"#{AGENT_PREFIX}/:run_id",
->(args) { handle_request(:get, args) }
)
add_route(
'forest_workflow_run_trigger',
'post',
"#{AGENT_PREFIX}/:run_id/trigger",
->(args) { handle_request(:post, args) }
)

self
end

def handle_request(method, args = {})
build(args)

base_url = configured_executor_url
run_id = args.dig(:params, 'run_id') || args.dig(:params, :run_id)
path = build_path(run_id, method)
response = forward(method, base_url, path, args)

{
content: response.body,
status: response.status,
headers: forwarded_response_headers(response)
}
end

private

def executor_configured?
url = ForestAdminAgent::Facades::Container.config_from_cache[:workflow_executor_url]
!(url.nil? || url.to_s.strip.empty?)
rescue StandardError
# Container not yet populated (e.g. boot-order edge case): treat as disabled.
false
end

def configured_executor_url
url = ForestAdminAgent::Facades::Container.config_from_cache[:workflow_executor_url]
if url.nil? || url.to_s.strip.empty?
raise Http::Exceptions::NotFoundError, 'Workflow executor proxy is not configured'
end

url.to_s.sub(%r{/+\z}, '')
end

def build_path(run_id, method)
suffix = method == :post ? '/trigger' : ''
"#{EXECUTOR_PREFIX}/#{run_id}#{suffix}"
end

def forward(method, base_url, path, args)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 4): forward [qlty:function-parameters]

query = forwarded_query_params(args[:params])
headers = forwarded_request_headers(args[:headers])
body = forwarded_body(method, args[:params])
target_url = "#{base_url}#{path}"

client = build_client(timeout_for(method))
client.run_request(method, target_url, body, headers) do |req|
req.params.update(query) unless query.empty?
end
rescue Faraday::TimeoutError => e
raise Http::Exceptions::ServiceUnavailableError.new('Workflow executor timed out', cause: e)
rescue Faraday::ConnectionFailed => e
raise Http::Exceptions::ServiceUnavailableError.new('Workflow executor unreachable', cause: e)
end

def timeout_for(method)
method == :get ? GET_TIMEOUT : TRIGGER_TIMEOUT
end

def build_client(request_timeout)
Faraday.new(request: { open_timeout: OPEN_TIMEOUT, timeout: request_timeout }) do |f|
f.request :json
f.response :json, content_type: /\bjson$/
f.adapter Faraday.default_adapter
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No timeout and open_timeout in the Faraday client ? (See https://lostisland.github.io/faraday/#/customization/request-options)
That depends ont the network layer between agent and executor, so exception might not be triggered.

end
end

# Strip Rails-injected routing keys; keep only true client query params.
def forwarded_query_params(params)
return {} unless params.is_a?(Hash)

params.each_with_object({}) do |(key, value), acc|
next if ROUTING_KEYS.include?(key.to_s)
next if value.is_a?(Hash) || value.is_a?(Array) # 'data' body, etc.

acc[key.to_s] = value
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 8): forwarded_query_params [qlty:function-complexity]

end

def forwarded_request_headers(headers)
return {} unless headers.is_a?(Hash)

FORWARDED_HEADERS.each_with_object({}) do |name, acc|
value = headers[name] || headers[name.downcase] || headers["HTTP_#{name.upcase}"]
acc[name] = value if value && !value.to_s.empty?
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 5): forwarded_request_headers [qlty:function-complexity]

end

def forwarded_body(method, params)
return nil if method == :get
return nil unless params.is_a?(Hash)

# JSON request bodies arrive parsed under :data when sent as JSON:API,
# or as the raw top-level params hash otherwise. Prefer :data when
# present; fall back to a sanitized copy of params.
body = params['data'] || params[:data]
return body if body

params.reject { |key, _| ROUTING_KEYS.include?(key.to_s) }
end

def forwarded_response_headers(response)
content_type = response.headers['content-type'] || response.headers['Content-Type']
content_type ? { 'Content-Type' => content_type } : {}
end
end
end
end
end

This file was deleted.

Loading
Loading