floe 0.16.0 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d592d8fe18169e547f72b096e1135e93775f15027614f7c9dcdd97714896b44
4
- data.tar.gz: dcad7afbd661be7f53566c143d48f9abd945ec15ceff24ed38d4c6ae14c34b3d
3
+ metadata.gz: 2b11d410b8a7518f7030bac6a5a29142167f54239e7cf1f966c6a82c2a191207
4
+ data.tar.gz: 793dd3f55b6d89b22d335831ca0a8663015ad520e8cf92603884f9168a64c8f6
5
5
  SHA512:
6
- metadata.gz: 1246c428448b2631a994095a0919cd533c1c5b52d814c13fab6ab1a9a54599388e6611eef3e472dd5cf330b43987e3b38e2833bba5dc5914cd14ea7e9bf22a47
7
- data.tar.gz: 576450ba34cdb2d263792c88b72f8710cc420ade67d28228f075917cb1ae6b9d27b90a6e9a278243960d3e562f58228d159214f06ec2fdfb9b895ee642ef9c7a
6
+ metadata.gz: c495de319815ea25d470262472e31675d016d5aa8d6ca761e64eb94556617809e7311b4dcce2242f1ccf0910c893679e44729357dca99b6b9b5299dff2fee0db
7
+ data.tar.gz: bcfdf7696caeaf80d6464dcc375747a40127c8327d1a75e486ad56e5d9618ef4f6ea73a3a5c35bb11f0f0a8c1e844b208b4a676f4555142cd7047c021d23dccf
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.0] - 2025-08-19
8
+ ### Fixed
9
+ - Fix credentials passed via CLI ([#307](https://github.com/ManageIQ/floe/pull/307))
10
+ - Fix nested PayloadTemplate interpolation ([#311](https://github.com/ManageIQ/floe/pull/311))
11
+
12
+ NOTE: Using `".$"` for hash keys is no longer required and will result in an error
13
+
14
+ ### Changed
15
+ - Allow credentials to be referenced from parameters ([#308](https://github.com/ManageIQ/floe/pull/308))
16
+
17
+ NOTE: Setting credentials via ResultPath now uses `$$.Credentials`
18
+
19
+ ### Added
20
+ - Add floe:// builtin runner and floe://http method ([#306](https://github.com/ManageIQ/floe/pull/306))
21
+
7
22
  ## [0.16.0] - 2025-04-08
8
23
  ### Added
9
24
  - Add Map state ItemBatcher/ItemSelector support ([#294](https://github.com/ManageIQ/floe/pull/294))
@@ -279,7 +294,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
279
294
  ### Added
280
295
  - Initial release
281
296
 
282
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.16.0...HEAD
297
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.17.0...HEAD
298
+ [0.16.0]: https://github.com/ManageIQ/floe/compare/v0.16.0...v0.17.0
283
299
  [0.16.0]: https://github.com/ManageIQ/floe/compare/v0.15.1...v0.16.0
284
300
  [0.15.1]: https://github.com/ManageIQ/floe/compare/v0.15.0...v0.15.1
285
301
  [0.15.0]: https://github.com/ManageIQ/floe/compare/v0.14.0...v0.15.0
data/README.md CHANGED
@@ -61,7 +61,7 @@ Or you can pass a file path with the `--credentials-file` parameter:
61
61
  bundle exec ruby exe/floe --workflow my-workflow.asl --credentials-file /tmp/20231218-80537-kj494t
62
62
  ```
63
63
 
64
- If you need to set a credential at runtime you can do that by using the `"ResultPath": "$.Credentials"` directive, for example to user a username/password to login and get a Bearer token:
64
+ If you need to set a credential at runtime you can do that by using the `"ResultPath": "$$.Credentials"` directive, for example to user a username/password to login and get a Bearer token:
65
65
 
66
66
  ```
67
67
  bundle exec ruby exe/floe --workflow my-workflow.asl --credentials='{"username": "user", "password": "pass"}'
@@ -78,14 +78,14 @@ bundle exec ruby exe/floe --workflow my-workflow.asl --credentials='{"username":
78
78
  "username.$": "$.username",
79
79
  "password.$": "$.password"
80
80
  },
81
- "ResultPath": "$.Credentials",
81
+ "ResultPath": "$$.Credentials",
82
82
  "Next": "DoSomething"
83
83
  },
84
84
  "DoSomething": {
85
85
  "Type": "Task",
86
86
  "Resource": "docker://do-something:latest",
87
87
  "Credentials": {
88
- "token.$": "$.bearer_token"
88
+ "token.$": "$$.Credentials.bearer_token"
89
89
  },
90
90
  "End": true
91
91
  }
@@ -155,6 +155,78 @@ until running_workflows.empty?
155
155
  end
156
156
  ```
157
157
 
158
+ ### Task Runners
159
+
160
+ Floe provides a number of options for the Task `"Resource"` parameter. The `"Resource"` runner is declared by the author based on the `URI`.
161
+
162
+ Task Runner Types:
163
+ * `floe://`
164
+ * `docker://`
165
+
166
+ #### Floe resource
167
+
168
+ This is the "builtin" runner and exposes methods that are executed internally without having to call out to a docker image.
169
+
170
+ ##### HTTP builtin method
171
+
172
+ `floe://http` allows you to execute HTTP method calls.
173
+
174
+ Example:
175
+ ```json
176
+ {
177
+ "Comment": "Execute a HTTP call",
178
+ "StartAt": "HTTP",
179
+ "States": {
180
+ "HTTP": {
181
+ "Type": "Task",
182
+ "Resource": "floe://http",
183
+ "Parameters": {
184
+ "Method": "POST",
185
+ "Url": "http://localhost:3000/api/login",
186
+ "Headers": {"ContentType": "application/json"},
187
+ "Body.$": {"username.$": "$$.Credentials.username", "password.$": "$$.Credentials.password"},
188
+ "Options": {"Encoding": "JSON"}
189
+ },
190
+ "ResultSelector": {"auth_token.$": "$.Body.auth_token"},
191
+ "ResultPath": "$$.Credentials",
192
+ "End": true
193
+ }
194
+ ```
195
+
196
+ HTTP Parameters:
197
+ * `Method` (required) - HTTP method name. Permitted values: `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS`, or `TRACE`
198
+ * `Url` (required) - URL to execute the HTTP call to
199
+ * `Headers` - Hash of unencoded HTTP request header key/value pairs.
200
+ * `QueryParameters` - URI query unencoded key/value pairs.
201
+ * `Body` - HTTP request body. Depending on Encoding this can be a String or a Hash of key/value pairs.
202
+ * `Ssl` - SSL options
203
+ * `Verify` - Boolean - Verify SSL certificate.
204
+ * `VerifyHostname` - Boolean - Verify SSL certificate hostname.
205
+ * `Hostname` - String - Server hostname for SNI.
206
+ * `CaFile` - String - Path to a CA file in PEM format.
207
+ * `CaPath` - String - Path to a CA directory.
208
+ * `VerifyMode` - Integer - OpenSSL constant.
209
+ * `VerifyDepth` - Integer - Maximum depth for the certificate chain validation.
210
+ * `Version` - Integer - SSL Version.
211
+ * `MinVersion` - Integer - Minimum SSL Version.
212
+ * `MaxVersion` - Integer - Maximum SSL Version.
213
+ * `Ciphers` - String - Ciphers supported.
214
+ * `Options`
215
+ * `Timeout`
216
+ * `ReadTimeout`
217
+ * `OpenTimeout`
218
+ * `WriteTimeout`
219
+ * `Encoding` - String
220
+ * `JSON` - JSON encodes the request and decodes the response
221
+ * `Proxy`
222
+ * `Uri` - String - URI of the proxy.
223
+ * `User` - String - User for the proxy.
224
+ * `Password` - String - Pasword for the proxy
225
+
226
+ #### Docker resource
227
+
228
+ The docker resource runner takes a docker image URI, including the registry, name, and tag.
229
+
158
230
  ### Docker Runner Options
159
231
 
160
232
  #### Docker
@@ -0,0 +1,146 @@
1
+ {
2
+ "Comment": "An example of the Amazon States Language with all states.",
3
+ "StartAt": "FirstState",
4
+ "States": {
5
+ "FirstState": {
6
+ "Type": "Task",
7
+ "Resource": "docker://docker.io/agrare/hello-world:latest",
8
+ "Credentials": {
9
+ "mysecret": "dont tell anyone"
10
+ },
11
+ "Retry": [
12
+ {
13
+ "ErrorEquals": [ "States.Timeout" ],
14
+ "IntervalSeconds": 3,
15
+ "MaxAttempts": 2,
16
+ "BackoffRate": 1.5
17
+ }
18
+ ],
19
+ "Catch": [
20
+ {
21
+ "ErrorEquals": [ "States.ALL" ],
22
+ "Next": "FailState"
23
+ }
24
+ ],
25
+ "Next": "ChoiceState"
26
+ },
27
+
28
+ "ChoiceState": {
29
+ "Type" : "Choice",
30
+ "Choices": [
31
+ {
32
+ "Variable": "$.foo",
33
+ "NumericEquals": 1,
34
+ "Next": "FirstMatchState"
35
+ },
36
+ {
37
+ "Variable": "$.foo",
38
+ "NumericEquals": 2,
39
+ "Next": "SecondMatchState"
40
+ },
41
+ {
42
+ "Variable": "$.foo",
43
+ "NumericEquals": 3,
44
+ "Next": "SuccessState"
45
+ }
46
+ ],
47
+ "Default": "FailState"
48
+ },
49
+
50
+ "FirstMatchState": {
51
+ "Type" : "Task",
52
+ "Resource": "docker://docker.io/agrare/hello-world:latest",
53
+ "Next": "PassState"
54
+ },
55
+
56
+ "SecondMatchState": {
57
+ "Type" : "Task",
58
+ "Resource": "docker://docker.io/agrare/hello-world:latest",
59
+ "Next": "WaitState"
60
+ },
61
+
62
+ "WaitState": {
63
+ "Type": "Wait",
64
+ "Seconds": 1,
65
+ "Next": "PassState"
66
+ },
67
+
68
+ "PassState": {
69
+ "Type": "Pass",
70
+ "Next": "MapState",
71
+ "Result": {
72
+ "foo": "bar",
73
+ "colors": [
74
+ "red",
75
+ "green",
76
+ "blue",
77
+ "yellow",
78
+ "white"
79
+ ]
80
+ }
81
+ },
82
+
83
+ "FailState": {
84
+ "Type": "Fail",
85
+ "Error": "FailStateError",
86
+ "Cause": "No Matches!"
87
+ },
88
+
89
+ "MapState": {
90
+ "Type": "Map",
91
+ "ItemsPath": "$.colors",
92
+ "MaxConcurrency": 2,
93
+ "ItemProcessor": {
94
+ "ProcessorConfig": {
95
+ "Mode": "INLINE"
96
+ },
97
+ "StartAt": "Generate UUID",
98
+ "States": {
99
+ "Generate UUID": {
100
+ "Type": "Pass",
101
+ "Next": "PassState",
102
+ "Parameters": {
103
+ "uuid.$": "States.UUID()"
104
+ }
105
+ },
106
+ "PassState": {
107
+ "Type": "Pass",
108
+ "End": true
109
+ }
110
+ }
111
+ },
112
+ "Next": "ParallelState"
113
+ },
114
+
115
+ "ParallelState": {
116
+ "Type": "Parallel",
117
+ "Next": "SuccessState",
118
+ "Branches": [
119
+ {
120
+ "StartAt": "Add",
121
+ "States": {
122
+ "Add": {
123
+ "Type": "Task",
124
+ "Resource": "docker://docker.io/agrare/sleep:latest",
125
+ "End": true
126
+ }
127
+ }
128
+ },
129
+ {
130
+ "StartAt": "Subtract",
131
+ "States": {
132
+ "Subtract": {
133
+ "Type": "Task",
134
+ "Resource": "docker://docker.io/agrare/sleep:latest",
135
+ "End": true
136
+ }
137
+ }
138
+ }
139
+ ]
140
+ },
141
+
142
+ "SuccessState": {
143
+ "Type": "Succeed"
144
+ }
145
+ }
146
+ }
data/examples/http.asl ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "Comment": "Execute a REST API call",
3
+ "StartAt": "Login",
4
+ "States": {
5
+ "Login": {
6
+ "Type": "Task",
7
+ "Resource": "floe://http",
8
+ "Parameters": {
9
+ "Method": "GET",
10
+ "Url": "http://localhost:3000/api/auth",
11
+ "Headers": {
12
+ "ContentType": "application/json",
13
+ "Authorization.$": "States.Format('Basic {}', States.Base64Encode(States.Format('{}:{}', $$.Credentials.username, $$.Credentials.password)))"
14
+ },
15
+ "Options": {"Encoding": "JSON"}
16
+ },
17
+ "ResultSelector": {"auth_token.$": "$.Body.auth_token"},
18
+ "ResultPath": "$$.Credentials",
19
+ "Next": "List VMs"
20
+ },
21
+ "List VMs": {
22
+ "Type": "Task",
23
+ "Resource": "floe://http",
24
+ "Parameters": {
25
+ "Method": "GET",
26
+ "Url": "http://localhost:3000/api/vms",
27
+ "Headers": {
28
+ "ContentType": "application/json",
29
+ "X-Auth-Token.$": "$$.Credentials.auth_token"
30
+ },
31
+ "Options": {"Encoding": "JSON"}
32
+ },
33
+ "ResultSelector": {
34
+ "Status.$": "$.Status",
35
+ "Resources.$": "$.Body.resources"
36
+ },
37
+ "End": true
38
+ }
39
+ }
40
+ }
@@ -8,7 +8,7 @@
8
8
  "Parameters": {
9
9
  "ECHO": "TOKEN"
10
10
  },
11
- "ResultPath": "$.Credentials",
11
+ "ResultPath": "$$.Credentials",
12
12
  "ResultSelector": {
13
13
  "bearer_token.$": "$.echo"
14
14
  },
data/floe.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.description = spec.summary
12
12
  spec.homepage = "https://github.com/ManageIQ/floe"
13
13
  spec.licenses = ["Apache-2.0"]
14
- spec.required_ruby_version = ">= 2.7.0"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
15
 
16
16
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
17
  spec.metadata['rubygems_mfa_required'] = "true"
@@ -38,6 +38,8 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "optimist", "~>3.0"
39
39
  spec.add_dependency "parslet", "~>2.0"
40
40
  spec.add_dependency "json", "~>2.10"
41
+ spec.add_dependency "faraday"
42
+ spec.add_dependency "faraday-follow_redirects"
41
43
 
42
44
  spec.add_development_dependency "manageiq-style", ">= 1.5.2"
43
45
  spec.add_development_dependency "rake", "~> 13.0"
@@ -0,0 +1,82 @@
1
+ module Floe
2
+ module BuiltinRunner
3
+ class Methods < BasicObject
4
+ def self.http(params, _secrets, _context)
5
+ error = http_verify_params(params)
6
+ return BuiltinRunner.error!({}, :cause => error) if error
7
+
8
+ method, url, headers, query, body =
9
+ params.values_at("Method", "Url", "Headers", "QueryParameters", "Body")
10
+
11
+ ssl = {
12
+ "verify" => params.dig("Ssl", "Verify"),
13
+ "verify_hostname" => params.dig("Ssl", "VerifyHostname"),
14
+ "hostname" => params.dig("Ssl", "Hostname"),
15
+ "ca_file" => params.dig("Ssl", "CaFile"),
16
+ "ca_path" => params.dig("Ssl", "CaPath"),
17
+ "verify_mode" => params.dig("Ssl", "VerifyMode"),
18
+ "verify_depth" => params.dig("Ssl", "VerifyDepth"),
19
+ "version" => params.dig("Ssl", "Version"),
20
+ "min_version" => params.dig("Ssl", "MinVersion"),
21
+ "max_version" => params.dig("Ssl", "MaxVersion"),
22
+ "ciphers" => params.dig("Ssl", "Ciphers")
23
+ }.compact
24
+
25
+ request = {
26
+ "timeout" => params.dig("Options", "Timeout"),
27
+ "read_timeout" => params.dig("Options", "ReadTimeout"),
28
+ "open_timeout" => params.dig("Options", "OpenTimeout"),
29
+ "write_timeout" => params.dig("Options", "WriteTimeout")
30
+ }.compact
31
+
32
+ proxy = {
33
+ "uri" => params.dig("Proxy", "Uri"),
34
+ "user" => params.dig("Proxy", "User"),
35
+ "password" => params.dig("Proxy", "Password")
36
+ }.compact
37
+
38
+ connection_options = {
39
+ :url => url,
40
+ :params => query,
41
+ :headers => headers,
42
+ :request => (request unless request.empty?),
43
+ :proxy => (proxy unless proxy.empty?),
44
+ :ssl => (ssl unless ssl.empty?)
45
+ }
46
+
47
+ require "faraday"
48
+ connection = ::Faraday.new(connection_options)
49
+
50
+ if params.dig("Options", "Encoding") == "JSON"
51
+ connection.request(:json)
52
+ connection.response(:json)
53
+ end
54
+
55
+ if params.dig("Options", "FollowRedirects") != false
56
+ require "faraday/follow_redirects"
57
+ connection.response(:follow_redirects)
58
+ end
59
+
60
+ response = connection.send(method.downcase) do |request|
61
+ request.body = body if body
62
+ end
63
+
64
+ output = {"Status" => response.status, "Body" => response.body, "Headers" => response.headers}
65
+
66
+ BuiltinRunner.success!({}, :output => output)
67
+ end
68
+
69
+ private_class_method def self.http_verify_params(params)
70
+ return "Missing Parameter: Url" if params["Url"].nil?
71
+ return "Missing Parameter: Method" if params["Method"].nil?
72
+ return "Invalid Parameter: Method: [#{params["Method"]}], must be GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, or TRACE" unless %w[GET POST PUT DELETE HEAD PATCH OPTIONS TRACE].include?(params["Method"])
73
+
74
+ nil
75
+ end
76
+
77
+ private_class_method def self.http_status!(runner_context)
78
+ runner_context
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ module Floe
2
+ module BuiltinRunner
3
+ class Runner < Floe::Runner
4
+ def run_async!(resource, params, secrets, context)
5
+ raise ArgumentError, "Invalid resource" unless resource&.start_with?(SCHEME_PREFIX)
6
+
7
+ method_name = resource.sub(SCHEME_PREFIX, "")
8
+
9
+ begin
10
+ runner_context = {"method" => method_name}
11
+ method_result = Methods.public_send(method_name, params, secrets, context)
12
+ method_result.merge(runner_context)
13
+ rescue NoMethodError
14
+ Floe::BuiltinRunner.error!(runner_context, :cause => "undefined method [#{method_name}]")
15
+ rescue => err
16
+ Floe::BuiltinRunner.error!(runner_context, :cause => err.to_s)
17
+ ensure
18
+ cleanup(runner_context)
19
+ end
20
+ end
21
+
22
+ def cleanup(runner_context)
23
+ method_name = runner_context["method"]
24
+ raise ArgumentError if method_name.nil?
25
+
26
+ cleanup_method = "#{method_name}_cleanup"
27
+ return unless Methods.respond_to?(cleanup_method, true)
28
+
29
+ Methods.send(cleanup_method, runner_context)
30
+ end
31
+
32
+ def status!(runner_context)
33
+ method_name = runner_context["method"]
34
+ raise ArgumentError if method_name.nil?
35
+ return if runner_context["running"] == false
36
+
37
+ Methods.send("#{method_name}_status!", runner_context)
38
+ end
39
+
40
+ def running?(runner_context)
41
+ runner_context["running"]
42
+ end
43
+
44
+ def success?(runner_context)
45
+ runner_context["success"]
46
+ end
47
+
48
+ def output(runner_context)
49
+ runner_context["output"]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ require "floe/builtin_runner/runner"
2
+ require "floe/builtin_runner/methods"
3
+
4
+ module Floe
5
+ module BuiltinRunner
6
+ SCHEME = "floe".freeze
7
+ SCHEME_PREFIX = "#{SCHEME}://".freeze
8
+
9
+ class << self
10
+ def error!(runner_context = {}, cause:, error: "States.TaskFailed")
11
+ runner_context.merge!(
12
+ "running" => false, "success" => false, "output" => {"Error" => error, "Cause" => cause}
13
+ )
14
+ end
15
+
16
+ def success!(runner_context = {}, output:)
17
+ runner_context.merge!(
18
+ "running" => false, "success" => true, "output" => output
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ Floe::Runner.register_scheme(Floe::BuiltinRunner::SCHEME, -> { Floe::BuiltinRunner::Runner.new })
data/lib/floe/cli.rb CHANGED
@@ -92,11 +92,14 @@ module Floe
92
92
  end
93
93
 
94
94
  def create_credentials(opts)
95
- if opts[:credentials_given]
96
- opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
97
- elsif opts[:credentials_file_given]
98
- File.read(opts[:credentials_file])
99
- end
95
+ credentials_str = if opts[:credentials_given]
96
+ opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
97
+ elsif opts[:credentials_file_given]
98
+ File.read(opts[:credentials_file])
99
+ else
100
+ return
101
+ end
102
+ JSON.parse(credentials_str)
100
103
  end
101
104
 
102
105
  def create_workflow(workflow, context_payload, input, credentials)
data/lib/floe/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Floe
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.0"
5
5
  end
@@ -5,15 +5,14 @@ module Floe
5
5
  class Context
6
6
  include Logging
7
7
 
8
- attr_accessor :credentials
9
-
10
8
  # @param context [Json|Hash] (default, create another with input and execution params)
11
9
  # @param input [Hash] (default: {})
12
- def initialize(context = nil, input: nil, credentials: {}, logger: nil)
10
+ def initialize(context = nil, input: nil, credentials: nil, logger: nil)
13
11
  context = JSON.parse(context) if context.kind_of?(String)
14
12
  input = JSON.parse(input || "{}")
15
13
 
16
14
  @context = context || {}
15
+ self["Credentials"] ||= credentials || {}
17
16
  self["Execution"] ||= {}
18
17
  self["Execution"]["Input"] ||= input
19
18
  self["State"] ||= {}
@@ -21,8 +20,6 @@ module Floe
21
20
  self["StateMachine"] ||= {}
22
21
  self["Task"] ||= {}
23
22
 
24
- @credentials = credentials || {}
25
-
26
23
  self.logger = logger if logger
27
24
  rescue JSON::ParserError => err
28
25
  raise Floe::InvalidExecutionInput, "Invalid State Machine Execution Input: #{err}: was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"
@@ -32,6 +29,10 @@ module Floe
32
29
  @context["Execution"]
33
30
  end
34
31
 
32
+ def credentials
33
+ @context["Credentials"]
34
+ end
35
+
35
36
  def started?
36
37
  execution.key?("StartTime")
37
38
  end
@@ -138,8 +139,18 @@ module Floe
138
139
  @context.dig(*args)
139
140
  end
140
141
 
142
+ def inspect
143
+ format("#<%s: %s>", self.class.name, safe_context.inspect)
144
+ end
145
+
141
146
  def to_h
142
- @context
147
+ safe_context
148
+ end
149
+
150
+ private
151
+
152
+ def safe_context
153
+ @context.except("Credentials")
143
154
  end
144
155
  end
145
156
  end
@@ -17,11 +17,9 @@ module Floe
17
17
 
18
18
  def parse_payload(value)
19
19
  case value
20
- when Array then parse_payload_array(value)
21
- when Hash then parse_payload_hash(value)
22
- when String then parse_payload_string(value)
23
- else
24
- value
20
+ when Array then parse_payload_array(value)
21
+ when Hash then parse_payload_hash(value)
22
+ else value
25
23
  end
26
24
  end
27
25
 
@@ -32,29 +30,40 @@ module Floe
32
30
  def parse_payload_hash(value)
33
31
  value.to_h do |key, val|
34
32
  if key.end_with?(".$")
35
- check_key_conflicts(key, value)
33
+ check_dynamic_datatype(key, val)
34
+ check_dynamic_key_conflicts(key, value)
36
35
 
37
- [key, parse_payload(val)]
36
+ [key, parse_dynamic_payload_string(key, val)]
38
37
  else
39
- [key, val]
38
+ [key, parse_payload(val)]
40
39
  end
41
40
  end
42
41
  end
43
42
 
44
- def parse_payload_string(value)
43
+ def parse_dynamic_payload_string(key, value)
45
44
  return Path.new(value) if Path.path?(value)
46
45
  return IntrinsicFunction.new(value) if IntrinsicFunction.intrinsic_function?(value)
47
46
 
48
- value
47
+ raise Floe::InvalidWorkflowError, "The value for the field \"#{key}\" must be a String that contains a valid Reference Path or Intrinsic Function expression"
48
+ end
49
+
50
+ def check_dynamic_datatype(key, value)
51
+ unless value.is_a?(String)
52
+ raise Floe::InvalidWorkflowError, "The value for the field \"#{key}\" must be a String that contains a valid Reference Path or Intrinsic Function expression"
53
+ end
54
+ end
55
+
56
+ def check_dynamic_key_conflicts(key, value)
57
+ if value.key?(key.chomp(".$"))
58
+ raise Floe::InvalidWorkflowError, "both #{key} and #{key.chomp(".$")} present"
59
+ end
49
60
  end
50
61
 
51
62
  def interpolate_value(value, context, inputs)
52
63
  case value
53
- when Array then interpolate_value_array(value, context, inputs)
54
- when Hash then interpolate_value_hash(value, context, inputs)
55
- when Path, IntrinsicFunction then value.value(context, inputs)
56
- else
57
- value
64
+ when Array then interpolate_value_array(value, context, inputs)
65
+ when Hash then interpolate_value_hash(value, context, inputs)
66
+ else value
58
67
  end
59
68
  end
60
69
 
@@ -65,17 +74,16 @@ module Floe
65
74
  def interpolate_value_hash(value, context, inputs)
66
75
  value.to_h do |key, val|
67
76
  if key.end_with?(".$")
68
- [key.chomp(".$"), interpolate_value(val, context, inputs)]
77
+ [key.chomp(".$"), interpolate_dynamic_value(val, context, inputs)]
69
78
  else
70
- [key, val]
79
+ [key, interpolate_value(val, context, inputs)]
71
80
  end
72
81
  end
73
82
  end
74
83
 
75
- def check_key_conflicts(key, value)
76
- if value.key?(key.chomp(".$"))
77
- raise Floe::InvalidWorkflowError, "both #{key} and #{key.chomp(".$")} present"
78
- end
84
+ def interpolate_dynamic_value(value, context, inputs)
85
+ # value will be a Path or IntrinsicFunction
86
+ value.value(context, inputs)
79
87
  end
80
88
  end
81
89
  end
@@ -8,10 +8,13 @@ module Floe
8
8
  def initialize(*)
9
9
  super
10
10
 
11
- raise Floe::InvalidWorkflowError, "Invalid Reference Path" if payload.match?(/@|,|:|\?/)
11
+ raise Floe::InvalidWorkflowError, "Invalid Reference Path" if payload.match?(/@|,|:|\?/)
12
+ raise Floe::InvalidWorkflowError, "Reference Path cannot start with $$" if payload.start_with?("$$") && !payload.start_with?("$$.Credentials")
13
+
14
+ path_shift = payload.start_with?("$$.Credentials") ? 3 : 1
12
15
 
13
16
  @path = JsonPath.new(payload)
14
- .path[1..]
17
+ .path[path_shift..]
15
18
  .map { |v| v.match(/\[(?<name>.+)\]/)["name"] }
16
19
  .filter_map { |v| v[0] == "'" ? v.delete("'") : v.to_i }
17
20
  end
@@ -49,7 +49,7 @@ module Floe
49
49
  end
50
50
 
51
51
  def each_child_context(context)
52
- context.state[child_context_key].map { |ctx| Context.new(ctx) }
52
+ context.state[child_context_key].map { |ctx| Context.new(ctx, :credentials => context.credentials) }
53
53
  end
54
54
  end
55
55
  end
@@ -15,9 +15,8 @@ module Floe
15
15
  return if output_path.nil?
16
16
 
17
17
  results = result_selector.value(context, results) if @result_selector
18
- if result_path.payload.start_with?("$.Credentials")
19
- credentials = result_path.set(context.credentials, results)["Credentials"]
20
- context.credentials.merge!(credentials)
18
+ if result_path.payload.match?(/^\$\$\.Credentials\b/)
19
+ context.credentials.merge!(result_path.set(context.credentials, results))
21
20
  output = context.input.dup
22
21
  else
23
22
  output = result_path.set(context.input.dup, results)
@@ -40,7 +40,8 @@ module Floe
40
40
  super
41
41
 
42
42
  input = process_input(context)
43
- runner_context = runner.run_async!(resource, input, credentials&.value({}, context.credentials), context)
43
+ secrets = credentials&.value(context, context.input)
44
+ runner_context = runner.run_async!(resource, input, secrets, context)
44
45
 
45
46
  context.state["RunnerContext"] = runner_context
46
47
  end
data/lib/floe/workflow.rb CHANGED
@@ -8,7 +8,7 @@ module Floe
8
8
  include Logging
9
9
 
10
10
  class << self
11
- def load(path_or_io, context = nil, credentials = {}, name = nil)
11
+ def load(path_or_io, context = nil, credentials = nil, name = nil)
12
12
  payload = path_or_io.respond_to?(:read) ? path_or_io.read : File.read(path_or_io)
13
13
  # default the name if it is a filename and none was passed in
14
14
  name ||= path_or_io.respond_to?(:read) ? "stream" : path_or_io.split("/").last.split(".").first
data/lib/floe.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "floe/null_logger"
6
6
  require_relative "floe/logging"
7
7
 
8
8
  require_relative "floe/runner"
9
+ require_relative "floe/builtin_runner"
9
10
 
10
11
  require_relative "floe/validation_mixin"
11
12
  require_relative "floe/workflow_base"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: awesome_spawn
@@ -107,6 +107,34 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '2.10'
110
+ - !ruby/object:Gem::Dependency
111
+ name: faraday
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: faraday-follow_redirects
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
110
138
  - !ruby/object:Gem::Dependency
111
139
  name: manageiq-style
112
140
  requirement: !ruby/object:Gem::Requirement
@@ -194,6 +222,8 @@ files:
194
222
  - LICENSE.txt
195
223
  - README.md
196
224
  - Rakefile
225
+ - examples/everything.asl
226
+ - examples/http.asl
197
227
  - examples/map.asl
198
228
  - examples/parallel.asl
199
229
  - examples/set-credential.asl
@@ -201,6 +231,9 @@ files:
201
231
  - exe/floe
202
232
  - floe.gemspec
203
233
  - lib/floe.rb
234
+ - lib/floe/builtin_runner.rb
235
+ - lib/floe/builtin_runner/methods.rb
236
+ - lib/floe/builtin_runner/runner.rb
204
237
  - lib/floe/cli.rb
205
238
  - lib/floe/container_runner.rb
206
239
  - lib/floe/container_runner/docker.rb
@@ -263,14 +296,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
263
296
  requirements:
264
297
  - - ">="
265
298
  - !ruby/object:Gem::Version
266
- version: 2.7.0
299
+ version: 3.0.0
267
300
  required_rubygems_version: !ruby/object:Gem::Requirement
268
301
  requirements:
269
302
  - - ">="
270
303
  - !ruby/object:Gem::Version
271
304
  version: '0'
272
305
  requirements: []
273
- rubygems_version: 3.6.6
306
+ rubygems_version: 3.6.7
274
307
  specification_version: 4
275
308
  summary: Floe is a runner for Amazon States Language workflows.
276
309
  test_files: []