conductor_ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +142 -0
- data/LICENSE +190 -0
- data/README.md +517 -0
- data/examples/agentic_workflows/llm_chat.rb +106 -0
- data/examples/dynamic_workflow.rb +177 -0
- data/examples/event_handler.rb +94 -0
- data/examples/event_listener_examples.rb +430 -0
- data/examples/helloworld/greetings_worker.rb +24 -0
- data/examples/helloworld/helloworld.rb +99 -0
- data/examples/kitchensink.rb +213 -0
- data/examples/metadata_journey.rb +189 -0
- data/examples/metrics_example.rb +284 -0
- data/examples/new_dsl_demo.rb +141 -0
- data/examples/orkes/http_poll.rb +83 -0
- data/examples/orkes/secrets_example.rb +69 -0
- data/examples/orkes/wait_for_webhook.rb +90 -0
- data/examples/prompt_journey.rb +245 -0
- data/examples/rag_workflow.rb +167 -0
- data/examples/schedule_journey.rb +244 -0
- data/examples/simple_worker.rb +125 -0
- data/examples/simple_workflow.rb +89 -0
- data/examples/task_context_example.rb +257 -0
- data/examples/task_listener_example.rb +192 -0
- data/examples/worker_configuration_example.rb +282 -0
- data/examples/workflow_dsl.rb +316 -0
- data/examples/workflow_ops.rb +305 -0
- data/lib/conductor/client/authorization_client.rb +238 -0
- data/lib/conductor/client/integration_client.rb +108 -0
- data/lib/conductor/client/metadata_client.rb +139 -0
- data/lib/conductor/client/prompt_client.rb +58 -0
- data/lib/conductor/client/scheduler_client.rb +132 -0
- data/lib/conductor/client/schema_client.rb +32 -0
- data/lib/conductor/client/secret_client.rb +48 -0
- data/lib/conductor/client/task_client.rb +168 -0
- data/lib/conductor/client/workflow_client.rb +242 -0
- data/lib/conductor/configuration/authentication_settings.rb +17 -0
- data/lib/conductor/configuration.rb +103 -0
- data/lib/conductor/exceptions.rb +86 -0
- data/lib/conductor/http/api/application_resource_api.rb +107 -0
- data/lib/conductor/http/api/authorization_resource_api.rb +56 -0
- data/lib/conductor/http/api/event_resource_api.rb +133 -0
- data/lib/conductor/http/api/gateway_auth_resource_api.rb +48 -0
- data/lib/conductor/http/api/group_resource_api.rb +76 -0
- data/lib/conductor/http/api/integration_resource_api.rb +145 -0
- data/lib/conductor/http/api/metadata_resource_api.rb +231 -0
- data/lib/conductor/http/api/prompt_resource_api.rb +81 -0
- data/lib/conductor/http/api/role_resource_api.rb +60 -0
- data/lib/conductor/http/api/scheduler_resource_api.rb +211 -0
- data/lib/conductor/http/api/schema_resource_api.rb +82 -0
- data/lib/conductor/http/api/secret_resource_api.rb +134 -0
- data/lib/conductor/http/api/task_resource_api.rb +321 -0
- data/lib/conductor/http/api/token_resource_api.rb +42 -0
- data/lib/conductor/http/api/user_resource_api.rb +59 -0
- data/lib/conductor/http/api/workflow_bulk_resource_api.rb +91 -0
- data/lib/conductor/http/api/workflow_resource_api.rb +451 -0
- data/lib/conductor/http/api_client.rb +437 -0
- data/lib/conductor/http/models/authentication_config.rb +67 -0
- data/lib/conductor/http/models/authorization_request.rb +39 -0
- data/lib/conductor/http/models/base_model.rb +162 -0
- data/lib/conductor/http/models/bulk_response.rb +39 -0
- data/lib/conductor/http/models/conductor_application.rb +39 -0
- data/lib/conductor/http/models/conductor_user.rb +53 -0
- data/lib/conductor/http/models/create_or_update_application_request.rb +24 -0
- data/lib/conductor/http/models/create_or_update_role_request.rb +27 -0
- data/lib/conductor/http/models/event_handler.rb +130 -0
- data/lib/conductor/http/models/generate_token_request.rb +27 -0
- data/lib/conductor/http/models/group.rb +36 -0
- data/lib/conductor/http/models/integration.rb +70 -0
- data/lib/conductor/http/models/integration_api.rb +53 -0
- data/lib/conductor/http/models/integration_api_update.rb +43 -0
- data/lib/conductor/http/models/integration_update.rb +36 -0
- data/lib/conductor/http/models/permission.rb +24 -0
- data/lib/conductor/http/models/poll_data.rb +33 -0
- data/lib/conductor/http/models/prompt_template.rb +59 -0
- data/lib/conductor/http/models/prompt_template_test_request.rb +43 -0
- data/lib/conductor/http/models/rerun_workflow_request.rb +37 -0
- data/lib/conductor/http/models/role.rb +27 -0
- data/lib/conductor/http/models/schema_def.rb +59 -0
- data/lib/conductor/http/models/search_result.rb +187 -0
- data/lib/conductor/http/models/skip_task_request.rb +27 -0
- data/lib/conductor/http/models/start_workflow_request.rb +68 -0
- data/lib/conductor/http/models/subject_ref.rb +35 -0
- data/lib/conductor/http/models/tag_object.rb +36 -0
- data/lib/conductor/http/models/target_ref.rb +39 -0
- data/lib/conductor/http/models/task.rb +156 -0
- data/lib/conductor/http/models/task_def.rb +95 -0
- data/lib/conductor/http/models/task_exec_log.rb +30 -0
- data/lib/conductor/http/models/task_result.rb +115 -0
- data/lib/conductor/http/models/task_result_status.rb +24 -0
- data/lib/conductor/http/models/token.rb +33 -0
- data/lib/conductor/http/models/upsert_group_request.rb +30 -0
- data/lib/conductor/http/models/upsert_user_request.rb +39 -0
- data/lib/conductor/http/models/workflow.rb +202 -0
- data/lib/conductor/http/models/workflow_def.rb +73 -0
- data/lib/conductor/http/models/workflow_schedule.rb +100 -0
- data/lib/conductor/http/models/workflow_state_update.rb +30 -0
- data/lib/conductor/http/models/workflow_status_constants.rb +57 -0
- data/lib/conductor/http/models/workflow_task.rb +169 -0
- data/lib/conductor/http/models/workflow_test_request.rb +67 -0
- data/lib/conductor/http/rest_client.rb +211 -0
- data/lib/conductor/orkes/models/access_key.rb +56 -0
- data/lib/conductor/orkes/models/granted_permission.rb +27 -0
- data/lib/conductor/orkes/models/metadata_tag.rb +15 -0
- data/lib/conductor/orkes/models/rate_limit_tag.rb +15 -0
- data/lib/conductor/orkes/orkes_clients.rb +69 -0
- data/lib/conductor/version.rb +5 -0
- data/lib/conductor/worker/events/conductor_event.rb +40 -0
- data/lib/conductor/worker/events/global_dispatcher.rb +37 -0
- data/lib/conductor/worker/events/http_events.rb +25 -0
- data/lib/conductor/worker/events/listener_registry.rb +40 -0
- data/lib/conductor/worker/events/listeners.rb +34 -0
- data/lib/conductor/worker/events/sync_event_dispatcher.rb +78 -0
- data/lib/conductor/worker/events/task_runner_events.rb +271 -0
- data/lib/conductor/worker/events/workflow_events.rb +49 -0
- data/lib/conductor/worker/fiber_executor.rb +532 -0
- data/lib/conductor/worker/ractor_task_runner.rb +501 -0
- data/lib/conductor/worker/task_context.rb +114 -0
- data/lib/conductor/worker/task_definition_registrar.rb +322 -0
- data/lib/conductor/worker/task_handler.rb +360 -0
- data/lib/conductor/worker/task_in_progress.rb +60 -0
- data/lib/conductor/worker/task_runner.rb +538 -0
- data/lib/conductor/worker/telemetry/metrics_collector.rb +196 -0
- data/lib/conductor/worker/telemetry/prometheus_backend.rb +224 -0
- data/lib/conductor/worker/worker.rb +355 -0
- data/lib/conductor/worker/worker_config.rb +154 -0
- data/lib/conductor/worker/worker_registry.rb +71 -0
- data/lib/conductor/workflow/dsl/input_ref.rb +37 -0
- data/lib/conductor/workflow/dsl/output_ref.rb +44 -0
- data/lib/conductor/workflow/dsl/parallel_builder.rb +49 -0
- data/lib/conductor/workflow/dsl/switch_builder.rb +74 -0
- data/lib/conductor/workflow/dsl/task_ref.rb +178 -0
- data/lib/conductor/workflow/dsl/workflow_builder.rb +1016 -0
- data/lib/conductor/workflow/dsl/workflow_definition.rb +150 -0
- data/lib/conductor/workflow/llm/chat_message.rb +47 -0
- data/lib/conductor/workflow/llm/embedding_model.rb +19 -0
- data/lib/conductor/workflow/llm/tool_call.rb +43 -0
- data/lib/conductor/workflow/llm/tool_spec.rb +46 -0
- data/lib/conductor/workflow/task_type.rb +68 -0
- data/lib/conductor/workflow/timeout_policy.rb +31 -0
- data/lib/conductor/workflow/workflow_executor.rb +373 -0
- data/lib/conductor.rb +192 -0
- metadata +359 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'uri'
|
|
7
|
+
require_relative '../configuration'
|
|
8
|
+
require_relative '../exceptions'
|
|
9
|
+
require_relative 'rest_client'
|
|
10
|
+
require_relative 'models/token'
|
|
11
|
+
|
|
12
|
+
module Conductor
|
|
13
|
+
module Http
|
|
14
|
+
# ApiClient handles HTTP communication and serialization/deserialization for Conductor API.
|
|
15
|
+
# It manages authentication tokens with automatic refresh and exponential backoff on failures.
|
|
16
|
+
class ApiClient
|
|
17
|
+
PRIMITIVE_TYPES = [String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
|
|
18
|
+
NATIVE_TYPE_MAPPING = {
|
|
19
|
+
'String' => String,
|
|
20
|
+
'Integer' => Integer,
|
|
21
|
+
'Float' => Float,
|
|
22
|
+
'Boolean' => :boolean,
|
|
23
|
+
'DateTime' => DateTime,
|
|
24
|
+
'Date' => Date,
|
|
25
|
+
'Time' => Time,
|
|
26
|
+
'Object' => Object
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :configuration, :rest_client, :last_response
|
|
30
|
+
attr_accessor :default_headers
|
|
31
|
+
|
|
32
|
+
# Initialize ApiClient
|
|
33
|
+
# @param [Configuration] configuration Configuration object
|
|
34
|
+
# @param [Hash] default_headers Optional default headers
|
|
35
|
+
def initialize(configuration: nil, default_headers: {})
|
|
36
|
+
@configuration = configuration || Configuration.new
|
|
37
|
+
@rest_client = RestClient.new(@configuration)
|
|
38
|
+
@default_headers = get_default_headers.merge(default_headers)
|
|
39
|
+
|
|
40
|
+
# Token refresh backoff tracking
|
|
41
|
+
@token_refresh_failures = 0
|
|
42
|
+
@last_token_refresh_attempt = 0
|
|
43
|
+
@max_token_refresh_failures = 5
|
|
44
|
+
|
|
45
|
+
# Mutex for thread-safe token refresh
|
|
46
|
+
@token_refresh_mutex = Mutex.new
|
|
47
|
+
|
|
48
|
+
# Initial token fetch
|
|
49
|
+
refresh_auth_token
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Main API call method with automatic retry on auth failures
|
|
53
|
+
# @param [String] resource_path The resource path
|
|
54
|
+
# @param [String] method HTTP method (GET, POST, PUT, DELETE, PATCH)
|
|
55
|
+
# @param [Hash] opts Optional parameters
|
|
56
|
+
# @option opts [Hash] :path_params Path parameters
|
|
57
|
+
# @option opts [Hash] :query_params Query parameters
|
|
58
|
+
# @option opts [Hash] :header_params Header parameters
|
|
59
|
+
# @option opts [Object] :body Request body
|
|
60
|
+
# @option opts [String] :return_type Expected return type
|
|
61
|
+
# @option opts [Boolean] :return_http_data_only Return only data (default: false)
|
|
62
|
+
# @return [Array, Object] Response data (and status/headers if return_http_data_only is false)
|
|
63
|
+
def call_api(resource_path, method, opts = {})
|
|
64
|
+
call_api_with_retry(resource_path, method, opts)
|
|
65
|
+
rescue AuthorizationError => e
|
|
66
|
+
if e.token_expired? || e.invalid_token?
|
|
67
|
+
token_status = e.token_expired? ? 'expired' : 'invalid'
|
|
68
|
+
logger.info("Authentication token is #{token_status}, renewing token... (request: #{method} #{resource_path})")
|
|
69
|
+
|
|
70
|
+
if force_refresh_auth_token
|
|
71
|
+
logger.debug('Authentication token successfully renewed')
|
|
72
|
+
# Retry the request once after successful token refresh
|
|
73
|
+
return call_api_no_retry(resource_path, method, opts)
|
|
74
|
+
else
|
|
75
|
+
logger.error('Failed to renew authentication token. Please check your credentials.')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
raise
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Sanitize object for serialization to JSON
|
|
82
|
+
# @param [Object] obj Object to sanitize
|
|
83
|
+
# @return [Object] Sanitized object ready for JSON serialization
|
|
84
|
+
def sanitize_for_serialization(obj)
|
|
85
|
+
return nil if obj.nil?
|
|
86
|
+
return obj if PRIMITIVE_TYPES.any? { |type| obj.is_a?(type) }
|
|
87
|
+
|
|
88
|
+
case obj
|
|
89
|
+
when Array
|
|
90
|
+
obj.map { |item| sanitize_for_serialization(item) }
|
|
91
|
+
when Hash
|
|
92
|
+
obj.transform_values do |val|
|
|
93
|
+
sanitize_for_serialization(val)
|
|
94
|
+
end
|
|
95
|
+
when DateTime, Date, Time
|
|
96
|
+
obj.iso8601
|
|
97
|
+
else
|
|
98
|
+
# Handle model objects with ATTRIBUTE_MAP and SWAGGER_TYPES
|
|
99
|
+
if obj.class.const_defined?(:ATTRIBUTE_MAP) && obj.class.const_defined?(:SWAGGER_TYPES)
|
|
100
|
+
attr_map = obj.class.const_get(:ATTRIBUTE_MAP)
|
|
101
|
+
swagger_types = obj.class.const_get(:SWAGGER_TYPES)
|
|
102
|
+
|
|
103
|
+
swagger_types.each_with_object({}) do |(attr, _type), hash|
|
|
104
|
+
value = obj.send(attr)
|
|
105
|
+
next if value.nil?
|
|
106
|
+
|
|
107
|
+
json_key = attr_map[attr]
|
|
108
|
+
hash[json_key] = sanitize_for_serialization(value)
|
|
109
|
+
end
|
|
110
|
+
elsif obj.respond_to?(:to_h)
|
|
111
|
+
sanitize_for_serialization(obj.to_h)
|
|
112
|
+
else
|
|
113
|
+
obj.to_s
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Deserialize HTTP response body into object
|
|
119
|
+
# @param [RestResponse] response HTTP response
|
|
120
|
+
# @param [String] return_type Expected return type (e.g., 'String', 'Array<Task>', 'Hash<String, Object>')
|
|
121
|
+
# @return [Object] Deserialized object
|
|
122
|
+
def deserialize(response, return_type)
|
|
123
|
+
return nil if response.nil? || return_type.nil?
|
|
124
|
+
|
|
125
|
+
body = response.body
|
|
126
|
+
return nil if body.nil? || body.empty?
|
|
127
|
+
|
|
128
|
+
# For String return type, return the raw body directly
|
|
129
|
+
# (many Conductor APIs return plain text, e.g. workflow ID)
|
|
130
|
+
return body.to_s.strip.delete_prefix('"').delete_suffix('"') if return_type == 'String'
|
|
131
|
+
|
|
132
|
+
# Parse response body as JSON for complex types
|
|
133
|
+
data = response.json
|
|
134
|
+
if data.nil?
|
|
135
|
+
# JSON parsing failed — try to use raw body
|
|
136
|
+
return body
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
deserialize_data(data, return_type)
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
logger.error("Failed to deserialize data into #{return_type}: #{e.message}")
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Force refresh authentication token (called on 401/403 errors)
|
|
146
|
+
# @return [Boolean] true if token was successfully refreshed, false otherwise
|
|
147
|
+
def force_refresh_auth_token
|
|
148
|
+
return false unless @configuration.auth_configured?
|
|
149
|
+
|
|
150
|
+
@token_refresh_mutex.synchronize do
|
|
151
|
+
# Skip backoff for legitimate token renewal (credentials should be valid)
|
|
152
|
+
token = get_new_token(skip_backoff: true)
|
|
153
|
+
if token
|
|
154
|
+
@configuration.update_token(token)
|
|
155
|
+
return true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check if auth was disabled during token refresh (404 response)
|
|
159
|
+
unless @configuration.auth_configured?
|
|
160
|
+
logger.info('Authentication was disabled (no auth endpoint found)')
|
|
161
|
+
return false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
false
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get authentication headers for requests
|
|
169
|
+
# @return [Hash, nil] Headers hash with X-Authorization or nil
|
|
170
|
+
def get_authentication_headers
|
|
171
|
+
return nil unless @configuration.auth_token
|
|
172
|
+
|
|
173
|
+
now_ms = (Time.now.to_f * 1000).round
|
|
174
|
+
time_since_last_update = now_ms - @configuration.token_update_time
|
|
175
|
+
|
|
176
|
+
# Proactively refresh token if TTL expired
|
|
177
|
+
if time_since_last_update > @configuration.auth_token_ttl_msec
|
|
178
|
+
@token_refresh_mutex.synchronize do
|
|
179
|
+
logger.info('Authentication token TTL expired, renewing token...')
|
|
180
|
+
token = get_new_token(skip_backoff: true)
|
|
181
|
+
@configuration.update_token(token) if token
|
|
182
|
+
logger.debug('Authentication token successfully renewed') if token
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
{ 'X-Authorization' => @configuration.auth_token }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
# Call API without automatic retry (internal method)
|
|
192
|
+
def call_api_no_retry(resource_path, method, opts = {})
|
|
193
|
+
path_params = opts[:path_params] || {}
|
|
194
|
+
query_params = opts[:query_params] || {}
|
|
195
|
+
header_params = (opts[:header_params] || {}).merge(@default_headers)
|
|
196
|
+
body = opts[:body]
|
|
197
|
+
return_type = opts[:return_type]
|
|
198
|
+
return_http_data_only = opts[:return_http_data_only] || false
|
|
199
|
+
|
|
200
|
+
metric_uri = resource_path
|
|
201
|
+
|
|
202
|
+
# Replace path parameters
|
|
203
|
+
path_params.each do |key, value|
|
|
204
|
+
resource_path = resource_path.sub("{#{key}}", URI.encode_www_form_component(value.to_s))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Add authentication headers (skip for /token endpoint)
|
|
208
|
+
if @configuration.auth_configured? && resource_path != '/token'
|
|
209
|
+
auth_headers = get_authentication_headers
|
|
210
|
+
header_params.merge!(auth_headers) if auth_headers
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Sanitize body for serialization
|
|
214
|
+
body = sanitize_for_serialization(body) if body
|
|
215
|
+
|
|
216
|
+
# Build full URL
|
|
217
|
+
url = @configuration.server_url + resource_path
|
|
218
|
+
|
|
219
|
+
# Make HTTP request
|
|
220
|
+
response = @rest_client.request(
|
|
221
|
+
method.to_s.upcase,
|
|
222
|
+
url,
|
|
223
|
+
query: query_params,
|
|
224
|
+
headers: header_params,
|
|
225
|
+
body: body ? JSON.generate(body) : nil,
|
|
226
|
+
metric_uri: metric_uri
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@last_response = response
|
|
230
|
+
|
|
231
|
+
# Deserialize response
|
|
232
|
+
return_data = return_type ? deserialize(response, return_type) : nil
|
|
233
|
+
|
|
234
|
+
if return_http_data_only
|
|
235
|
+
return_data
|
|
236
|
+
else
|
|
237
|
+
[return_data, response.status, response.headers]
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Wrapper to handle auth retry
|
|
242
|
+
alias call_api_with_retry call_api_no_retry
|
|
243
|
+
|
|
244
|
+
# Refresh authentication token on initialization
|
|
245
|
+
def refresh_auth_token
|
|
246
|
+
return if @configuration.auth_token
|
|
247
|
+
return unless @configuration.auth_configured?
|
|
248
|
+
|
|
249
|
+
@token_refresh_mutex.synchronize do
|
|
250
|
+
token = get_new_token(skip_backoff: false)
|
|
251
|
+
@configuration.update_token(token) if token || @configuration.auth_configured?
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Get new token from server with exponential backoff
|
|
256
|
+
# @param [Boolean] skip_backoff Skip backoff logic for legitimate renewals
|
|
257
|
+
# @return [String, nil] Token string or nil
|
|
258
|
+
def get_new_token(skip_backoff: false)
|
|
259
|
+
# Apply backoff only if not skipping and we have failures
|
|
260
|
+
unless skip_backoff
|
|
261
|
+
if @token_refresh_failures >= @max_token_refresh_failures
|
|
262
|
+
logger.error(
|
|
263
|
+
"Token refresh has failed #{@token_refresh_failures} times. " \
|
|
264
|
+
'Please check your authentication credentials. ' \
|
|
265
|
+
'Stopping token refresh attempts.'
|
|
266
|
+
)
|
|
267
|
+
return nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Exponential backoff: 2^failures seconds
|
|
271
|
+
if @token_refresh_failures.positive?
|
|
272
|
+
now = Time.now.to_f
|
|
273
|
+
backoff_seconds = 2**@token_refresh_failures
|
|
274
|
+
time_since_last_attempt = now - @last_token_refresh_attempt
|
|
275
|
+
|
|
276
|
+
if time_since_last_attempt < backoff_seconds
|
|
277
|
+
remaining = backoff_seconds - time_since_last_attempt
|
|
278
|
+
logger.warn(
|
|
279
|
+
"Token refresh backoff active. Please wait #{remaining.round(1)}s before next attempt. " \
|
|
280
|
+
"(Failure count: #{@token_refresh_failures})"
|
|
281
|
+
)
|
|
282
|
+
return nil
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
@last_token_refresh_attempt = Time.now.to_f
|
|
288
|
+
|
|
289
|
+
begin
|
|
290
|
+
key_id = @configuration.authentication_settings.key_id
|
|
291
|
+
key_secret = @configuration.authentication_settings.key_secret
|
|
292
|
+
|
|
293
|
+
if key_id.nil? || key_secret.nil?
|
|
294
|
+
logger.error('Authentication Key or Secret is not set. Failed to get the auth token')
|
|
295
|
+
@token_refresh_failures += 1
|
|
296
|
+
return nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
logger.debug('Requesting new authentication token from server')
|
|
300
|
+
|
|
301
|
+
response = call_api_no_retry(
|
|
302
|
+
'/token',
|
|
303
|
+
'POST',
|
|
304
|
+
header_params: { 'Content-Type' => 'application/json' },
|
|
305
|
+
body: { keyId: key_id, keySecret: key_secret },
|
|
306
|
+
return_type: 'Token',
|
|
307
|
+
return_http_data_only: true
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Success - reset failure counter
|
|
311
|
+
@token_refresh_failures = 0
|
|
312
|
+
response.token
|
|
313
|
+
rescue AuthorizationError => e
|
|
314
|
+
# 401 from /token endpoint - invalid credentials
|
|
315
|
+
@token_refresh_failures += 1
|
|
316
|
+
logger.error(
|
|
317
|
+
"Authentication failed when getting token (attempt #{@token_refresh_failures}): " \
|
|
318
|
+
"#{e.status} - #{e.code}. " \
|
|
319
|
+
'Please check your CONDUCTOR_AUTH_KEY and CONDUCTOR_AUTH_SECRET. ' \
|
|
320
|
+
"Will retry with exponential backoff (#{2**@token_refresh_failures}s)."
|
|
321
|
+
)
|
|
322
|
+
nil
|
|
323
|
+
rescue ApiError => e
|
|
324
|
+
# Check if it's a 404 - indicates no authentication endpoint (Conductor OSS)
|
|
325
|
+
if e.not_found?
|
|
326
|
+
logger.info(
|
|
327
|
+
'Authentication endpoint /token not found (404). ' \
|
|
328
|
+
'Running in open mode without authentication (Conductor OSS).'
|
|
329
|
+
)
|
|
330
|
+
# Disable authentication to prevent future attempts
|
|
331
|
+
@configuration.disable_auth!
|
|
332
|
+
# Reset failure counter since this is not a failure
|
|
333
|
+
@token_refresh_failures = 0
|
|
334
|
+
else
|
|
335
|
+
# Other API errors
|
|
336
|
+
@token_refresh_failures += 1
|
|
337
|
+
logger.error(
|
|
338
|
+
"API error when getting token (attempt #{@token_refresh_failures}): " \
|
|
339
|
+
"#{e.status} - #{e.reason}"
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
nil
|
|
343
|
+
rescue StandardError => e
|
|
344
|
+
# Other errors (network, etc)
|
|
345
|
+
@token_refresh_failures += 1
|
|
346
|
+
logger.error("Failed to get new token (attempt #{@token_refresh_failures}): #{e.message}")
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Deserialize data into specified type
|
|
352
|
+
# @param [Object] data Data to deserialize
|
|
353
|
+
# @param [String] type_string Type string (e.g., 'Task', 'Array<Task>', 'Hash<String, Integer>')
|
|
354
|
+
# @return [Object] Deserialized object
|
|
355
|
+
def deserialize_data(data, type_string)
|
|
356
|
+
return nil if data.nil?
|
|
357
|
+
|
|
358
|
+
# Handle Array types: Array<Type>
|
|
359
|
+
if type_string.start_with?('Array<')
|
|
360
|
+
sub_type = type_string[6..-2] # Extract Type from Array<Type>
|
|
361
|
+
return data.map { |item| deserialize_data(item, sub_type) } if data.is_a?(Array)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Handle Hash types: Hash<KeyType, ValueType>
|
|
365
|
+
if type_string.start_with?('Hash<')
|
|
366
|
+
match = type_string.match(/Hash<([^,]+),\s*(.+)>/)
|
|
367
|
+
if match
|
|
368
|
+
_key_type = match[1]
|
|
369
|
+
value_type = match[2]
|
|
370
|
+
return data.transform_values { |v| deserialize_data(v, value_type) } if data.is_a?(Hash)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Handle primitive types
|
|
375
|
+
case type_string
|
|
376
|
+
when 'String'
|
|
377
|
+
data.to_s
|
|
378
|
+
when 'Integer'
|
|
379
|
+
data.to_i
|
|
380
|
+
when 'Float'
|
|
381
|
+
data.to_f
|
|
382
|
+
when 'Boolean'
|
|
383
|
+
data == true || data.to_s.downcase == 'true'
|
|
384
|
+
when 'DateTime'
|
|
385
|
+
DateTime.parse(data.to_s)
|
|
386
|
+
when 'Date'
|
|
387
|
+
Date.parse(data.to_s)
|
|
388
|
+
when 'Time'
|
|
389
|
+
Time.parse(data.to_s)
|
|
390
|
+
when 'Object'
|
|
391
|
+
data
|
|
392
|
+
else
|
|
393
|
+
# Try to deserialize as a model class
|
|
394
|
+
deserialize_model(data, type_string)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Deserialize data into model class
|
|
399
|
+
# @param [Hash] data Data hash
|
|
400
|
+
# @param [String] class_name Model class name
|
|
401
|
+
# @return [Object] Model instance
|
|
402
|
+
def deserialize_model(data, class_name)
|
|
403
|
+
return data unless data.is_a?(Hash)
|
|
404
|
+
|
|
405
|
+
# Try to get the model class from Conductor::Http::Models
|
|
406
|
+
klass = begin
|
|
407
|
+
Conductor::Http::Models.const_get(class_name)
|
|
408
|
+
rescue NameError
|
|
409
|
+
return data
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Use BaseModel.from_hash if available
|
|
413
|
+
if klass.respond_to?(:from_hash)
|
|
414
|
+
klass.from_hash(data)
|
|
415
|
+
else
|
|
416
|
+
data
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Get default headers
|
|
421
|
+
def get_default_headers
|
|
422
|
+
{
|
|
423
|
+
'Content-Type' => 'application/json',
|
|
424
|
+
'Accept' => 'application/json'
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Get logger
|
|
429
|
+
def logger
|
|
430
|
+
@logger ||= begin
|
|
431
|
+
require 'logger'
|
|
432
|
+
Logger.new($stdout, level: Logger::INFO)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conductor
|
|
4
|
+
module Http
|
|
5
|
+
module Models
|
|
6
|
+
# Authentication type constants
|
|
7
|
+
module AuthenticationType
|
|
8
|
+
NONE = 'NONE'
|
|
9
|
+
API_KEY = 'API_KEY'
|
|
10
|
+
OIDC = 'OIDC'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# AuthenticationConfig model - gateway authentication configuration
|
|
14
|
+
class AuthenticationConfig < BaseModel
|
|
15
|
+
SWAGGER_TYPES = {
|
|
16
|
+
id: 'String',
|
|
17
|
+
application_id: 'String',
|
|
18
|
+
authentication_type: 'String',
|
|
19
|
+
api_keys: 'Array<String>',
|
|
20
|
+
audience: 'String',
|
|
21
|
+
conductor_token: 'String',
|
|
22
|
+
created_by: 'String',
|
|
23
|
+
fallback_to_default_auth: 'Boolean',
|
|
24
|
+
issuer_uri: 'String',
|
|
25
|
+
passthrough: 'Boolean',
|
|
26
|
+
token_in_workflow_input: 'Boolean',
|
|
27
|
+
updated_by: 'String'
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
ATTRIBUTE_MAP = {
|
|
31
|
+
id: :id,
|
|
32
|
+
application_id: :applicationId,
|
|
33
|
+
authentication_type: :authenticationType,
|
|
34
|
+
api_keys: :apiKeys,
|
|
35
|
+
audience: :audience,
|
|
36
|
+
conductor_token: :conductorToken,
|
|
37
|
+
created_by: :createdBy,
|
|
38
|
+
fallback_to_default_auth: :fallbackToDefaultAuth,
|
|
39
|
+
issuer_uri: :issuerUri,
|
|
40
|
+
passthrough: :passthrough,
|
|
41
|
+
token_in_workflow_input: :tokenInWorkflowInput,
|
|
42
|
+
updated_by: :updatedBy
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
attr_accessor :id, :application_id, :authentication_type, :api_keys,
|
|
46
|
+
:audience, :conductor_token, :created_by,
|
|
47
|
+
:fallback_to_default_auth, :issuer_uri, :passthrough,
|
|
48
|
+
:token_in_workflow_input, :updated_by
|
|
49
|
+
|
|
50
|
+
def initialize(params = {})
|
|
51
|
+
@id = params[:id]
|
|
52
|
+
@application_id = params[:application_id]
|
|
53
|
+
@authentication_type = params[:authentication_type]
|
|
54
|
+
@api_keys = params[:api_keys]
|
|
55
|
+
@audience = params[:audience]
|
|
56
|
+
@conductor_token = params[:conductor_token]
|
|
57
|
+
@created_by = params[:created_by]
|
|
58
|
+
@fallback_to_default_auth = params[:fallback_to_default_auth]
|
|
59
|
+
@issuer_uri = params[:issuer_uri]
|
|
60
|
+
@passthrough = params[:passthrough]
|
|
61
|
+
@token_in_workflow_input = params[:token_in_workflow_input]
|
|
62
|
+
@updated_by = params[:updated_by]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conductor
|
|
4
|
+
module Http
|
|
5
|
+
module Models
|
|
6
|
+
# Access type constants for authorization
|
|
7
|
+
module AccessType
|
|
8
|
+
CREATE = 'CREATE'
|
|
9
|
+
READ = 'READ'
|
|
10
|
+
UPDATE = 'UPDATE'
|
|
11
|
+
DELETE = 'DELETE'
|
|
12
|
+
EXECUTE = 'EXECUTE'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# AuthorizationRequest model - request to grant or revoke permissions
|
|
16
|
+
class AuthorizationRequest < BaseModel
|
|
17
|
+
SWAGGER_TYPES = {
|
|
18
|
+
subject: 'SubjectRef',
|
|
19
|
+
target: 'TargetRef',
|
|
20
|
+
access: 'Array<String>'
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
ATTRIBUTE_MAP = {
|
|
24
|
+
subject: :subject,
|
|
25
|
+
target: :target,
|
|
26
|
+
access: :access
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
attr_accessor :subject, :target, :access
|
|
30
|
+
|
|
31
|
+
def initialize(params = {})
|
|
32
|
+
@subject = params[:subject]
|
|
33
|
+
@target = params[:target]
|
|
34
|
+
@access = params[:access]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Conductor
|
|
7
|
+
module Http
|
|
8
|
+
module Models
|
|
9
|
+
# Base class for all Conductor model objects
|
|
10
|
+
# Implements the SWAGGER_TYPES pattern from Python SDK
|
|
11
|
+
class BaseModel
|
|
12
|
+
# Class method to define swagger types and attribute mappings
|
|
13
|
+
def self.swagger_types
|
|
14
|
+
self::SWAGGER_TYPES
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.attribute_map
|
|
18
|
+
self::ATTRIBUTE_MAP
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convert model to hash using ATTRIBUTE_MAP for JSON keys
|
|
22
|
+
def to_h
|
|
23
|
+
hash = {}
|
|
24
|
+
self.class.attribute_map.each do |attr, json_key|
|
|
25
|
+
value = send(attr)
|
|
26
|
+
next if value.nil?
|
|
27
|
+
|
|
28
|
+
hash[json_key.to_s] = serialize_value(value)
|
|
29
|
+
end
|
|
30
|
+
hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Alias for to_h
|
|
34
|
+
alias to_hash to_h
|
|
35
|
+
|
|
36
|
+
# Convert model to JSON string
|
|
37
|
+
def to_json(*_args)
|
|
38
|
+
JSON.generate(to_h)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build model from hash (deserialization)
|
|
42
|
+
def self.from_hash(hash)
|
|
43
|
+
return nil unless hash
|
|
44
|
+
# If it's not a Hash (e.g., a string expression like "${workflow.input.foo}"),
|
|
45
|
+
# return it as-is rather than trying to deserialize
|
|
46
|
+
return hash unless hash.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
instance = new
|
|
49
|
+
attribute_map.each do |attr, json_key|
|
|
50
|
+
json_key_str = json_key.to_s
|
|
51
|
+
next unless hash.key?(json_key_str) || hash.key?(json_key.to_sym)
|
|
52
|
+
|
|
53
|
+
value = hash[json_key_str] || hash[json_key.to_sym]
|
|
54
|
+
type = swagger_types[attr]
|
|
55
|
+
|
|
56
|
+
instance.send("#{attr}=", deserialize_value(value, type))
|
|
57
|
+
end
|
|
58
|
+
instance
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Build model from JSON string
|
|
62
|
+
def self.from_json(json_string)
|
|
63
|
+
from_hash(JSON.parse(json_string))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Serialize a value for JSON output
|
|
69
|
+
def serialize_value(value)
|
|
70
|
+
case value
|
|
71
|
+
when BaseModel
|
|
72
|
+
value.to_h
|
|
73
|
+
when Array
|
|
74
|
+
value.map { |v| serialize_value(v) }
|
|
75
|
+
when Hash
|
|
76
|
+
value.transform_values { |v| serialize_value(v) }
|
|
77
|
+
when Time, DateTime, Date
|
|
78
|
+
value.iso8601
|
|
79
|
+
else
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Deserialize a value based on type string
|
|
85
|
+
def self.deserialize_value(value, type)
|
|
86
|
+
return nil if value.nil?
|
|
87
|
+
return value if type.nil?
|
|
88
|
+
|
|
89
|
+
# Handle array types: "Array<Type>"
|
|
90
|
+
if type.start_with?('Array<')
|
|
91
|
+
inner_type = type[6..-2] # Extract Type from Array<Type>
|
|
92
|
+
return value.map { |v| deserialize_value(v, inner_type) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Handle hash types: "Hash<String, Type>" or "Hash{String => Type}"
|
|
96
|
+
if type.start_with?('Hash<', 'Hash{')
|
|
97
|
+
# Extract value type from Hash<K, V> or Hash{K => V}
|
|
98
|
+
match = type.match(/Hash[<{].*,\s*(.+)[>}]/)
|
|
99
|
+
value_type = match[1] if match
|
|
100
|
+
return value.transform_values { |v| deserialize_value(v, value_type) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Handle primitive types
|
|
104
|
+
case type
|
|
105
|
+
when 'String'
|
|
106
|
+
value.to_s
|
|
107
|
+
when 'Integer'
|
|
108
|
+
value.to_i
|
|
109
|
+
when 'Float'
|
|
110
|
+
value.to_f
|
|
111
|
+
when 'Boolean', 'BOOLEAN'
|
|
112
|
+
value.to_s.downcase == 'true'
|
|
113
|
+
when 'DateTime'
|
|
114
|
+
parse_datetime(value)
|
|
115
|
+
when 'Date'
|
|
116
|
+
Date.parse(value.to_s)
|
|
117
|
+
when 'Time'
|
|
118
|
+
Time.parse(value.to_s)
|
|
119
|
+
when 'Object'
|
|
120
|
+
value
|
|
121
|
+
else
|
|
122
|
+
# Model class - convert to proper class
|
|
123
|
+
deserialize_model(value, type)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.parse_datetime(value)
|
|
128
|
+
return value if value.is_a?(DateTime) || value.is_a?(Time)
|
|
129
|
+
|
|
130
|
+
# Try ISO8601 first
|
|
131
|
+
DateTime.iso8601(value.to_s)
|
|
132
|
+
rescue ArgumentError
|
|
133
|
+
# Fall back to regular parsing
|
|
134
|
+
DateTime.parse(value.to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.deserialize_model(value, type)
|
|
138
|
+
# If value is not a Hash (e.g., a string expression), return as-is
|
|
139
|
+
return value unless value.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
# Try to find the model class
|
|
142
|
+
klass = find_model_class(type)
|
|
143
|
+
return value unless klass
|
|
144
|
+
|
|
145
|
+
# If it's already the right type, return it
|
|
146
|
+
return value if value.is_a?(klass)
|
|
147
|
+
|
|
148
|
+
# Deserialize hash to model
|
|
149
|
+
klass.respond_to?(:from_hash) ? klass.from_hash(value) : value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.find_model_class(type)
|
|
153
|
+
# Try to find class in Conductor::Http::Models namespace
|
|
154
|
+
const_name = type.split('::').last
|
|
155
|
+
Conductor::Http::Models.const_get(const_name) if Conductor::Http::Models.const_defined?(const_name)
|
|
156
|
+
rescue NameError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|