floe 0.15.1 → 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: 6f21cf2fcc6a7fa9f7536e0cca328d101a867c389f53f768e4746b6e69f643a5
4
- data.tar.gz: f3d82401842f66504afdeaa4b051db01d5fcb9e79b704af368712d68b4950251
3
+ metadata.gz: 2b11d410b8a7518f7030bac6a5a29142167f54239e7cf1f966c6a82c2a191207
4
+ data.tar.gz: 793dd3f55b6d89b22d335831ca0a8663015ad520e8cf92603884f9168a64c8f6
5
5
  SHA512:
6
- metadata.gz: 3e73dbdd868ec57b8138a9e4b0b529c0f2c44cdb21d43c0ae29ca9094512f4044cc621a38e4ac217bf6b17e13f1ef3a361cf5917023eab87d30c7b4cc90eccfc
7
- data.tar.gz: e174fabde62d6348f8517f7c2d019600b34612f5b8d822eb83c2c5579f5e224596b6edaec383bfe9cce75c3260e50a4a0b744764119cd96e2a973f92b49b7d5a
6
+ metadata.gz: c495de319815ea25d470262472e31675d016d5aa8d6ca761e64eb94556617809e7311b4dcce2242f1ccf0910c893679e44729357dca99b6b9b5299dff2fee0db
7
+ data.tar.gz: bcfdf7696caeaf80d6464dcc375747a40127c8327d1a75e486ad56e5d9618ef4f6ea73a3a5c35bb11f0f0a8c1e844b208b4a676f4555142cd7047c021d23dccf
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ 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
+
22
+ ## [0.16.0] - 2025-04-08
23
+ ### Added
24
+ - Add Map state ItemBatcher/ItemSelector support ([#294](https://github.com/ManageIQ/floe/pull/294))
25
+
26
+ ### Fixed
27
+ - Fix JSON.parse exception expectation ([#300](https://github.com/ManageIQ/floe/pull/300))
28
+
7
29
  ## [0.15.1] - 2024-11-21
8
30
  ### Fixed
9
31
  - Fix Map/Parallel States checking container_runner#status! of finished states ([#296](https://github.com/ManageIQ/floe/pull/296))
@@ -272,7 +294,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
272
294
  ### Added
273
295
  - Initial release
274
296
 
275
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.15.1...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
299
+ [0.16.0]: https://github.com/ManageIQ/floe/compare/v0.15.1...v0.16.0
276
300
  [0.15.1]: https://github.com/ManageIQ/floe/compare/v0.15.0...v0.15.1
277
301
  [0.15.0]: https://github.com/ManageIQ/floe/compare/v0.14.0...v0.15.0
278
302
  [0.14.0]: https://github.com/ManageIQ/floe/compare/v0.13.1...v0.14.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"
@@ -37,6 +37,9 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency "kubeclient", "~>4.7"
38
38
  spec.add_dependency "optimist", "~>3.0"
39
39
  spec.add_dependency "parslet", "~>2.0"
40
+ spec.add_dependency "json", "~>2.10"
41
+ spec.add_dependency "faraday"
42
+ spec.add_dependency "faraday-follow_redirects"
40
43
 
41
44
  spec.add_development_dependency "manageiq-style", ">= 1.5.2"
42
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.15.1"
4
+ VERSION = "0.17.0"
5
5
  end
@@ -80,27 +80,27 @@ module Floe
80
80
  # rubocop:enable Naming/PredicateName
81
81
  # rubocop:enable Style/OptionalBooleanParameter
82
82
 
83
- def equals?(lhs, rhs)
83
+ def op_equals?(lhs, rhs)
84
84
  lhs == rhs
85
85
  end
86
86
 
87
- def lessthan?(lhs, rhs)
87
+ def op_lessthan?(lhs, rhs)
88
88
  lhs < rhs
89
89
  end
90
90
 
91
- def greaterthan?(lhs, rhs)
91
+ def op_greaterthan?(lhs, rhs)
92
92
  lhs > rhs
93
93
  end
94
94
 
95
- def lessthanequals?(lhs, rhs)
95
+ def op_lessthanequals?(lhs, rhs)
96
96
  lhs <= rhs
97
97
  end
98
98
 
99
- def greaterthanequals?(lhs, rhs)
99
+ def op_greaterthanequals?(lhs, rhs)
100
100
  lhs >= rhs
101
101
  end
102
102
 
103
- def matches?(lhs, rhs)
103
+ def op_matches?(lhs, rhs)
104
104
  lhs.match?(Regexp.escape(rhs).gsub('\*', '.*?'))
105
105
  end
106
106
 
@@ -111,7 +111,7 @@ module Floe
111
111
  if (match_values = OPERATION.match(key))
112
112
  @compare_key = key
113
113
  @type, operator, @path = match_values.captures
114
- @operation = "#{operator.downcase}?".to_sym
114
+ @operation = "op_#{operator.downcase}?".to_sym
115
115
  @compare_predicate = parse_predicate(type)
116
116
  break
117
117
  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
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ItemBatcher
6
+ include ValidationMixin
7
+
8
+ attr_reader :name, :batch_input, :max_items_per_batch, :max_items_per_batch_path, :max_input_bytes_per_batch, :max_input_bytes_per_batch_path
9
+
10
+ def initialize(payload, name)
11
+ @name = name
12
+
13
+ @batch_input = PayloadTemplate.new(payload["BatchInput"]) if payload["BatchInput"]
14
+ @max_items_per_batch = payload["MaxItemsPerBatch"]
15
+ @max_input_bytes_per_batch = payload["MaxInputBytesPerBatch"]
16
+
17
+ @max_items_per_batch_path = ReferencePath.new(payload["MaxItemsPerBatchPath"]) if payload["MaxItemsPerBatchPath"]
18
+ @max_input_bytes_per_batch_path = ReferencePath.new(payload["MaxInputBytesPerBatchPath"]) if payload["MaxInputBytesPerBatchPath"]
19
+
20
+ validate!
21
+ end
22
+
23
+ def value(context, input, state_input = nil)
24
+ state_input ||= input
25
+
26
+ output = batch_input ? batch_input.value(context, state_input) : {}
27
+
28
+ input.each_slice(max_items(context, state_input)).map do |batch|
29
+ output.merge("Items" => batch)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def max_items(context, state_input)
36
+ return max_items_per_batch if max_items_per_batch
37
+ return if max_items_per_batch_path.nil?
38
+
39
+ result = max_items_per_batch_path.value(context, state_input)
40
+ raise runtime_field_error!("MaxItemsPerBatchPath", result, "must be a positive integer") if result.nil? || !result.kind_of?(Integer) || result <= 0
41
+
42
+ result
43
+ end
44
+
45
+ def validate!
46
+ if [max_items_per_batch, max_items_per_batch_path].all?(&:nil?)
47
+ parser_error!("must have one of \"MaxItemsPerBatch\", \"MaxItemsPerBatchPath\"")
48
+ end
49
+
50
+ parser_error!("must not specify both \"MaxItemsPerBatch\" and \"MaxItemsPerBatchPath\"") if max_items_per_batch && max_items_per_batch_path
51
+ parser_error!("must not specify both \"MaxInputBytesPerBatch\" and \"MaxInputBytesPerBatchPath\"") if max_input_bytes_per_batch && max_input_bytes_per_batch_path
52
+
53
+ if max_items_per_batch && (!max_items_per_batch.kind_of?(Integer) || max_items_per_batch <= 0)
54
+ invalid_field_error!("MaxItemsPerBatch", max_items_per_batch, "must be a positive integer")
55
+ end
56
+ if max_input_bytes_per_batch && (!max_input_bytes_per_batch.kind_of?(Integer) || max_input_bytes_per_batch <= 0)
57
+ invalid_field_error!("MaxInputBytesPerBatch", max_input_bytes_per_batch, "must be a positive integer")
58
+ end
59
+ end
60
+ end
61
+ end
62
+ 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)
@@ -31,8 +31,8 @@ module Floe
31
31
  @item_processor = ItemProcessor.new(payload["ItemProcessor"], name)
32
32
  @items_path = ReferencePath.new(payload.fetch("ItemsPath", "$"))
33
33
  @item_reader = payload["ItemReader"]
34
- @item_selector = payload["ItemSelector"]
35
- @item_batcher = payload["ItemBatcher"]
34
+ @item_selector = PayloadTemplate.new(payload["ItemSelector"]) if payload["ItemSelector"]
35
+ @item_batcher = ItemBatcher.new(payload["ItemBatcher"], name + ["ItemBatcher"]) if payload["ItemBatcher"]
36
36
  @result_writer = payload["ResultWriter"]
37
37
  @max_concurrency = payload["MaxConcurrency"]&.to_i
38
38
  @tolerated_failure_percentage = payload["ToleratedFailurePercentage"]&.to_i
@@ -43,7 +43,9 @@ module Floe
43
43
 
44
44
  def process_input(context)
45
45
  input = super
46
- items_path.value(context, input)
46
+ input = items_path.value(context, input)
47
+ input = item_batcher.value(context, input, context.state["Input"]) if item_batcher
48
+ input
47
49
  end
48
50
 
49
51
  def start(context)
@@ -51,7 +53,20 @@ module Floe
51
53
 
52
54
  input = process_input(context)
53
55
 
54
- context.state["ItemProcessorContext"] = input.map { |item| Context.new({"Execution" => {"Id" => context.execution["Id"]}}, :input => item.to_json).to_h }
56
+ context.state["ItemProcessorContext"] = input.map.with_index do |item, index|
57
+ item_processor_context = {
58
+ "Execution" => {
59
+ "Id" => context.execution["Id"]
60
+ },
61
+ "Map" => {
62
+ "Item" => {"Index" => index, "Value" => item}
63
+ }
64
+ }
65
+
66
+ item_processor_input = item_selector ? item_selector.value(item_processor_context, context.state["Input"]) : item
67
+
68
+ Context.new(item_processor_context, :input => item_processor_input.to_json).to_h
69
+ end
55
70
  end
56
71
 
57
72
  def end?
@@ -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"
@@ -20,6 +21,7 @@ require_relative "floe/workflow/choice_rule/or"
20
21
  require_relative "floe/workflow/choice_rule/and"
21
22
  require_relative "floe/workflow/choice_rule/data"
22
23
  require_relative "floe/workflow/context"
24
+ require_relative "floe/workflow/item_batcher"
23
25
  require_relative "floe/workflow/item_processor"
24
26
  require_relative "floe/workflow/intrinsic_function"
25
27
  require_relative "floe/workflow/intrinsic_function/parser"
data/renovate.json CHANGED
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": [
4
- "config:base"
5
- ]
3
+ "inheritConfig": true,
4
+ "inheritConfigRepoName": "manageiq/renovate-config"
6
5
  }
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-11-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: awesome_spawn
@@ -94,6 +93,48 @@ dependencies:
94
93
  - - "~>"
95
94
  - !ruby/object:Gem::Version
96
95
  version: '2.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: json
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.10'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
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'
97
138
  - !ruby/object:Gem::Dependency
98
139
  name: manageiq-style
99
140
  requirement: !ruby/object:Gem::Requirement
@@ -165,7 +206,6 @@ dependencies:
165
206
  - !ruby/object:Gem::Version
166
207
  version: '0'
167
208
  description: Floe is a runner for Amazon States Language workflows.
168
- email:
169
209
  executables:
170
210
  - floe
171
211
  extensions: []
@@ -182,6 +222,8 @@ files:
182
222
  - LICENSE.txt
183
223
  - README.md
184
224
  - Rakefile
225
+ - examples/everything.asl
226
+ - examples/http.asl
185
227
  - examples/map.asl
186
228
  - examples/parallel.asl
187
229
  - examples/set-credential.asl
@@ -189,6 +231,9 @@ files:
189
231
  - exe/floe
190
232
  - floe.gemspec
191
233
  - lib/floe.rb
234
+ - lib/floe/builtin_runner.rb
235
+ - lib/floe/builtin_runner/methods.rb
236
+ - lib/floe/builtin_runner/runner.rb
192
237
  - lib/floe/cli.rb
193
238
  - lib/floe/container_runner.rb
194
239
  - lib/floe/container_runner/docker.rb
@@ -213,6 +258,7 @@ files:
213
258
  - lib/floe/workflow/intrinsic_function.rb
214
259
  - lib/floe/workflow/intrinsic_function/parser.rb
215
260
  - lib/floe/workflow/intrinsic_function/transformer.rb
261
+ - lib/floe/workflow/item_batcher.rb
216
262
  - lib/floe/workflow/item_processor.rb
217
263
  - lib/floe/workflow/path.rb
218
264
  - lib/floe/workflow/payload_template.rb
@@ -243,7 +289,6 @@ metadata:
243
289
  homepage_uri: https://github.com/ManageIQ/floe
244
290
  source_code_uri: https://github.com/ManageIQ/floe
245
291
  changelog_uri: https://github.com/ManageIQ/floe/blob/master/CHANGELOG.md
246
- post_install_message:
247
292
  rdoc_options: []
248
293
  require_paths:
249
294
  - lib
@@ -251,15 +296,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
251
296
  requirements:
252
297
  - - ">="
253
298
  - !ruby/object:Gem::Version
254
- version: 2.7.0
299
+ version: 3.0.0
255
300
  required_rubygems_version: !ruby/object:Gem::Requirement
256
301
  requirements:
257
302
  - - ">="
258
303
  - !ruby/object:Gem::Version
259
304
  version: '0'
260
305
  requirements: []
261
- rubygems_version: 3.4.20
262
- signing_key:
306
+ rubygems_version: 3.6.7
263
307
  specification_version: 4
264
308
  summary: Floe is a runner for Amazon States Language workflows.
265
309
  test_files: []