floe 0.11.2 → 0.11.3

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: 302eabcce76893426ed95920bff11ed2bba9956a7d006249d86212e30c5cc67c
4
- data.tar.gz: 4148b20da8ef81e6e45a4ec73489cdcf956115d1f99535019aef3b561ed45e00
3
+ metadata.gz: cb1a786256a191ba7d1b3b08f6e3cd02fa778fcf28ab44a7353f36ae7979039b
4
+ data.tar.gz: ca5ab5d0b5ed1949945593bb1f07141e554fbbac4d825cbf6333bb92e5b17b6d
5
5
  SHA512:
6
- metadata.gz: 73a1a714b1d729af89dd9839655955f76c3ed69fa8814e230fc03746175d1ff243b8b58fd02dce272d683b39fe50cd88c1c3a7f05cd2e36e9aa070747f85ae6f
7
- data.tar.gz: bf13a6c8f07a5918e78d99bb5a03c29b3a76e059c92cd6f3fec85dd64b1d701138a16a9c0e6df10ed820f4f97ef932152f1d0a61b38a60d3d344728da9ec33c7
6
+ metadata.gz: e96574a58740f8659f27947144bdc5067fbade1645a0b651e66607494fce5b0a16ef762bbba1638d54a8804425c2de2c3938884b4c18346be3623b4ffbdc821b
7
+ data.tar.gz: da673ecc866d1df2f6f75872df183beda957996fd54ff09177ebd84b27bc64fade88122eaaf34f7134e3a92d74b9d9f7a7322648042df444ad889de9a053d298
data/.codeclimate.yml ADDED
@@ -0,0 +1,16 @@
1
+ prepare:
2
+ fetch:
3
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
4
+ path: ".rubocop_base.yml"
5
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_cc_base.yml
6
+ path: ".rubocop_cc_base.yml"
7
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/base.yml
8
+ path: styles/base.yml
9
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
10
+ path: styles/cc_base.yml
11
+ plugins:
12
+ rubocop:
13
+ enabled: true
14
+ config: ".rubocop_cc.yml"
15
+ channel: rubocop-1-56-3
16
+ version: '2'
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.11.3] - 2024-06-20
8
+ ### Fixed
9
+ - ResultPath=$ replaces complete output ([#199](https://github.com/ManageIQ/floe/pull/199))
10
+ - Fix retrier backoff values ([#200](https://github.com/ManageIQ/floe/pull/200))
11
+ - Fix Retry issues ([#202](https://github.com/ManageIQ/floe/pull/202))
12
+ - Add Apache-2.0 license ([#217](https://github.com/ManageIQ/floe/pull/217))
13
+
14
+ ### Changed
15
+ - Update gemspec summary ([#205](https://github.com/ManageIQ/floe/pull/205))
16
+ - Simpler State#long_name ([#204](https://github.com/ManageIQ/floe/pull/204))
17
+ - State only modifies Context#state - prep for Map/Parallel ([#206](https://github.com/ManageIQ/floe/pull/206))
18
+ - Set StateHistory in Workflow not State ([#211](https://github.com/ManageIQ/floe/pull/211))
19
+ - Make Runner#wait optional ([#190](https://github.com/ManageIQ/floe/pull/190))
20
+ - Pass credentials around with context ([#203](https://github.com/ManageIQ/floe/pull/203))
21
+ - Pass context to State without workflow ([#216](https://github.com/ManageIQ/floe/pull/216))
22
+ - Move the guts of the CLI into a class for easy testing ([#220](https://github.com/ManageIQ/floe/pull/220))
23
+
24
+ ### Added
25
+ - Set State PreviousStateGuid in StateHistory ([#208](https://github.com/ManageIQ/floe/pull/208))
26
+ - Add a codeclimate config file ([#224](https://github.com/ManageIQ/floe/pull/224))
27
+ - Add an Execution unique ID to Context ([#226](https://github.com/ManageIQ/floe/pull/226))
28
+
7
29
  ## [0.11.2] - 2024-05-24
8
30
  ### Fixed
9
31
  - Output now based upon raw input not effective input ([#191](https://github.com/ManageIQ/floe/pull/191))
@@ -177,7 +199,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
177
199
  ### Added
178
200
  - Initial release
179
201
 
180
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.11.2...HEAD
202
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.11.3...HEAD
203
+ [0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
181
204
  [0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
182
205
  [0.11.1]: https://github.com/ManageIQ/floe/compare/v0.11.0...v0.11.1
183
206
  [0.11.0]: https://github.com/ManageIQ/floe/compare/v0.10.0...v0.11.0
data/Gemfile CHANGED
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  plugin "bundler-inject", "~> 2.0"
6
- require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil
6
+ require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil # rubocop:disable Style/RescueModifier
7
7
 
8
8
  # Specify your gem's dependencies in floe.gemspec
9
9
  gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md CHANGED
@@ -148,7 +148,7 @@ until running_workflows.empty?
148
148
  ready_workflows = Floe::Workflow.wait(running_workflows)
149
149
  # Step through the ready workflows until they would block
150
150
  ready_workflows.each do |workflow|
151
- loop while workflow.step_nonblock == 0
151
+ workflow.run_nonblock
152
152
  end
153
153
  # Remove any finished workflows from the list of running_workflows
154
154
  running_workflows.reject!(&:end?)
@@ -206,3 +206,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
206
206
  ## Contributing
207
207
 
208
208
  Bug reports and pull requests are welcome on GitHub at https://github.com/ManageIQ/floe.
209
+
210
+ ## License
211
+
212
+ The gem is available as open source under the terms of the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
data/exe/floe CHANGED
@@ -3,75 +3,6 @@
3
3
 
4
4
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
5
 
6
- require "optimist"
7
- require "floe"
8
- require "floe/container_runner"
9
-
10
- opts = Optimist.options do
11
- version("v#{Floe::VERSION}\n")
12
- usage("[options] workflow input [workflow2 input2]")
13
-
14
- opt :workflow, "Path to your workflow json file (alternative to passing a bare workflow)", :type => :string
15
- opt :input, <<~EOMSG, :type => :string
16
- JSON payload of the Input to the workflow
17
- If --input is passed and --workflow is not passed, will be used for all bare workflows listed.
18
- If --input is not passed and --workflow is passed, defaults to '{}'.
19
- EOMSG
20
- opt :context, "JSON payload of the Context", :type => :string
21
- opt :credentials, "JSON payload with Credentials", :type => :string
22
- opt :credentials_file, "Path to a file with Credentials", :type => :string
23
-
24
- Floe::ContainerRunner.cli_options(self)
25
-
26
- banner("")
27
- banner("General options:")
28
- end
29
-
30
- # Create workflow/input pairs from the various combinations of paramaters
31
- args =
32
- if opts[:workflow_given]
33
- Optimist.die("cannot specify both --workflow and bare workflows") if ARGV.any?
34
-
35
- [opts[:workflow], opts.fetch(:input, "{}")]
36
- elsif opts[:input_given]
37
- Optimist.die("workflow(s) must be specified") if ARGV.empty?
38
-
39
- ARGV.flat_map { |w| [w, opts[:input].dup] }
40
- else
41
- Optimist.die("workflow/input pairs must be specified") if ARGV.empty? || (ARGV.size > 1 && ARGV.size.odd?)
42
-
43
- ARGV
44
- end
45
-
46
- Floe::ContainerRunner.resolve_cli_options!(opts)
47
-
48
- require "logger"
49
- Floe.logger = Logger.new($stdout)
50
-
51
- credentials =
52
- if opts[:credentials_given]
53
- opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
54
- elsif opts[:credentials_file_given]
55
- File.read(opts[:credentials_file])
56
- end
57
-
58
- workflows =
59
- args.each_slice(2).map do |workflow, input|
60
- context = Floe::Workflow::Context.new(opts[:context], :input => input)
61
- Floe::Workflow.load(workflow, context, credentials)
62
- end
63
-
64
- # run
65
-
66
- Floe::Workflow.wait(workflows, &:run_nonblock)
67
-
68
- # display status
69
-
70
- workflows.each do |workflow|
71
- puts "", "#{workflow.name}#{" (#{workflow.status})" unless workflow.context.success?}", "===" if workflows.size > 1
72
- puts workflow.output.inspect
73
- end
74
-
75
- # exit status
76
-
77
- exit workflows.all? { |workflow| workflow.context.success? } ? 0 : 1
6
+ require "floe/cli"
7
+ success = Floe::CLI.new.run(ARGV)
8
+ exit(success ? 0 : 1)
data/floe.gemspec CHANGED
@@ -7,9 +7,10 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Floe::VERSION
8
8
  spec.authors = ["ManageIQ Developers"]
9
9
 
10
- spec.summary = "Simple Workflow Runner."
11
- spec.description = "Simple Workflow Runner."
10
+ spec.summary = "Floe is a runner for Amazon States Language workflows."
11
+ spec.description = spec.summary
12
12
  spec.homepage = "https://github.com/ManageIQ/floe"
13
+ spec.licenses = ["Apache-2.0"]
13
14
  spec.required_ruby_version = ">= 2.7.0"
14
15
 
15
16
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
data/lib/floe/cli.rb ADDED
@@ -0,0 +1,83 @@
1
+ module Floe
2
+ class CLI
3
+ def initialize
4
+ require "optimist"
5
+ require "floe"
6
+ require "floe/container_runner"
7
+ require "logger"
8
+
9
+ Floe.logger = Logger.new($stdout)
10
+ end
11
+
12
+ def run(args = ARGV)
13
+ workflows_inputs, opts = parse_options!(args)
14
+
15
+ credentials =
16
+ if opts[:credentials_given]
17
+ opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
18
+ elsif opts[:credentials_file_given]
19
+ File.read(opts[:credentials_file])
20
+ end
21
+
22
+ workflows =
23
+ workflows_inputs.each_slice(2).map do |workflow, input|
24
+ context = Floe::Workflow::Context.new(opts[:context], :input => input, :credentials => credentials)
25
+ Floe::Workflow.load(workflow, context)
26
+ end
27
+
28
+ Floe::Workflow.wait(workflows, &:run_nonblock)
29
+
30
+ # Display status
31
+ workflows.each do |workflow|
32
+ puts "", "#{workflow.name}#{" (#{workflow.status})" unless workflow.context.success?}", "===" if workflows.size > 1
33
+ puts workflow.output.inspect
34
+ end
35
+
36
+ workflows.all? { |workflow| workflow.context.success? }
37
+ end
38
+
39
+ private
40
+
41
+ def parse_options!(args)
42
+ opts = Optimist.options(args) do
43
+ version("v#{Floe::VERSION}\n")
44
+ usage("[options] workflow input [workflow2 input2]")
45
+
46
+ opt :workflow, "Path to your workflow json file (alternative to passing a bare workflow)", :type => :string
47
+ opt :input, <<~EOMSG, :type => :string
48
+ JSON payload of the Input to the workflow
49
+ If --input is passed and --workflow is not passed, will be used for all bare workflows listed.
50
+ If --input is not passed and --workflow is passed, defaults to '{}'.
51
+ EOMSG
52
+ opt :context, "JSON payload of the Context", :type => :string
53
+ opt :credentials, "JSON payload with Credentials", :type => :string
54
+ opt :credentials_file, "Path to a file with Credentials", :type => :string
55
+
56
+ Floe::ContainerRunner.cli_options(self)
57
+
58
+ banner("")
59
+ banner("General options:")
60
+ end
61
+
62
+ # Create workflow/input pairs from the various combinations of paramaters
63
+ workflows_inputs =
64
+ if opts[:workflow_given]
65
+ Optimist.die("cannot specify both --workflow and bare workflows") if args.any?
66
+
67
+ [opts[:workflow], opts.fetch(:input, "{}")]
68
+ elsif opts[:input_given]
69
+ Optimist.die("workflow(s) must be specified") if args.empty?
70
+
71
+ args.flat_map { |w| [w, opts[:input].dup] }
72
+ else
73
+ Optimist.die("workflow/input pairs must be specified") if args.empty? || (args.size > 1 && args.size.odd?)
74
+
75
+ args
76
+ end
77
+
78
+ Floe::ContainerRunner.resolve_cli_options!(opts)
79
+
80
+ return workflows_inputs, opts
81
+ end
82
+ end
83
+ end
data/lib/floe/runner.rb CHANGED
@@ -6,7 +6,7 @@ module Floe
6
6
 
7
7
  OUTPUT_MARKER = "__FLOE_OUTPUT__\n"
8
8
 
9
- def initialize(_options = {})
9
+ def initialize(_options = {}) # rubocop:disable Style/RedundantInitialize
10
10
  end
11
11
 
12
12
  @runners = {}
@@ -75,8 +75,13 @@ module Floe
75
75
  raise NotImplementedError, "Must be implemented in a subclass"
76
76
  end
77
77
 
78
- def wait(timeout: nil, events: %i[create update delete])
79
- raise NotImplementedError, "Must be implemented in a subclass"
80
- end
78
+ # Optional Watcher for events that is run in another thread.
79
+ #
80
+ # @yield [event, runner_context]
81
+ # @yieldparam [Symbol] event values: :create :update :delete :unknown
82
+ # @yieldparam [Hash] runner_context context provided by runner
83
+ # def wait(timeout: nil, events: %i[create update delete])
84
+ # raise NotImplementedError, "Must be implemented in a subclass"
85
+ # end
81
86
  end
82
87
  end
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.11.2"
4
+ VERSION = "0.11.3"
5
5
  end
@@ -3,9 +3,11 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class Context
6
+ attr_accessor :credentials
7
+
6
8
  # @param context [Json|Hash] (default, create another with input and execution params)
7
9
  # @param input [Hash] (default: {})
8
- def initialize(context = nil, input: nil)
10
+ def initialize(context = nil, input: nil, credentials: {})
9
11
  context = JSON.parse(context) if context.kind_of?(String)
10
12
 
11
13
  input ||= {}
@@ -18,6 +20,10 @@ module Floe
18
20
  self["StateHistory"] ||= []
19
21
  self["StateMachine"] ||= {}
20
22
  self["Task"] ||= {}
23
+
24
+ @credentials = credentials || {}
25
+ rescue JSON::ParserError => err
26
+ raise Floe::InvalidWorkflowError, err.message
21
27
  end
22
28
 
23
29
  def execution
@@ -84,6 +90,16 @@ module Floe
84
90
  status == "success"
85
91
  end
86
92
 
93
+ def state_started?
94
+ state.key?("EnteredTime")
95
+ end
96
+
97
+ # State#running? also checks docker to see if it is running.
98
+ # You possibly want to use that instead
99
+ def state_finished?
100
+ state.key?("FinishedTime")
101
+ end
102
+
87
103
  def state=(val)
88
104
  @context["State"] = val
89
105
  end
@@ -25,12 +25,9 @@ module Floe
25
25
  def set(context, value)
26
26
  result = context.dup
27
27
 
28
- # If the payload is '$' then merge the value into the context
29
- # otherwise store the value under the path
30
- #
31
- # TODO: how to handle non-hash values, raise error if path=$ and value not a hash?
28
+ # If the payload is '$' then replace the output with the value
32
29
  if path.empty?
33
- result.merge!(value)
30
+ result = value.dup
34
31
  else
35
32
  child = result
36
33
  keys = path.dup
@@ -14,8 +14,9 @@ module Floe
14
14
  @backoff_rate = payload["BackoffRate"] || 2.0
15
15
  end
16
16
 
17
+ # @param [Integer] attempt 1 for the first attempt
17
18
  def sleep_duration(attempt)
18
- interval_seconds * (backoff_rate * attempt)
19
+ interval_seconds * (backoff_rate**(attempt - 1))
19
20
  end
20
21
  end
21
22
  end
@@ -20,95 +20,79 @@ module Floe
20
20
  end
21
21
  end
22
22
 
23
- attr_reader :workflow, :comment, :name, :type, :payload
23
+ attr_reader :comment, :name, :type, :payload
24
24
 
25
25
  def initialize(workflow, name, payload)
26
- @workflow = workflow
27
26
  @name = name
28
27
  @payload = payload
29
28
  @type = payload["Type"]
30
29
  @comment = payload["Comment"]
31
30
 
32
31
  raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
33
- raise Floe::InvalidWorkflowError, "State name [#{name}] must be less than or equal to 80 characters" if name.length > 80
32
+ raise Floe::InvalidWorkflowError, "State name [#{name[..79]}...] must be less than or equal to 80 characters" if name.length > 80
34
33
  end
35
34
 
36
- def wait(timeout: nil)
35
+ def wait(context, timeout: nil)
37
36
  start = Time.now.utc
38
37
 
39
38
  loop do
40
- return 0 if ready?
39
+ return 0 if ready?(context)
41
40
  return Errno::EAGAIN if timeout && (timeout.zero? || Time.now.utc - start > timeout)
42
41
 
43
42
  sleep(1)
44
43
  end
45
44
  end
46
45
 
47
- def run_nonblock!
48
- start(context.input) unless started?
49
- return Errno::EAGAIN unless ready?
46
+ # @return for incomplete Errno::EAGAIN, for completed 0
47
+ def run_nonblock!(context)
48
+ start(context) unless context.state_started?
49
+ return Errno::EAGAIN unless ready?(context)
50
50
 
51
- finish
51
+ finish(context)
52
52
  end
53
53
 
54
- def start(_input)
55
- start_time = Time.now.utc.iso8601
56
-
57
- context.execution["StartTime"] ||= start_time
58
- context.state["Guid"] = SecureRandom.uuid
59
- context.state["EnteredTime"] = start_time
54
+ def start(context)
55
+ context.state["EnteredTime"] = Time.now.utc.iso8601
60
56
 
61
57
  logger.info("Running state: [#{long_name}] with input [#{context.input}]...")
62
58
  end
63
59
 
64
- def finish
60
+ def finish(context)
65
61
  finished_time = Time.now.utc
66
- finished_time_iso = finished_time.iso8601
67
62
  entered_time = Time.parse(context.state["EnteredTime"])
68
63
 
69
- context.state["FinishedTime"] ||= finished_time_iso
64
+ context.state["FinishedTime"] ||= finished_time.iso8601
70
65
  context.state["Duration"] = finished_time - entered_time
71
- context.execution["EndTime"] = finished_time_iso if context.next_state.nil?
72
66
 
73
67
  level = context.output&.[]("Error") ? :error : :info
74
68
  logger.public_send(level, "Running state: [#{long_name}] with input [#{context.input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.output}]")
75
69
 
76
- context.state_history << context.state
77
-
78
70
  0
79
71
  end
80
72
 
81
- def context
82
- workflow.context
83
- end
84
-
85
- def started?
86
- context.state.key?("EnteredTime")
87
- end
88
-
89
- def ready?
90
- !started? || !running?
73
+ def ready?(context)
74
+ !context.state_started? || !running?(context)
91
75
  end
92
76
 
93
- def finished?
94
- context.state.key?("FinishedTime")
77
+ def running?(context)
78
+ raise NotImplementedError, "Must be implemented in a subclass"
95
79
  end
96
80
 
97
- def waiting?
81
+ def waiting?(context)
98
82
  context.state["WaitUntil"] && Time.now.utc <= Time.parse(context.state["WaitUntil"])
99
83
  end
100
84
 
101
- def wait_until
85
+ def wait_until(context)
102
86
  context.state["WaitUntil"] && Time.parse(context.state["WaitUntil"])
103
87
  end
104
88
 
105
89
  def long_name
106
- "#{self.class.name.split("::").last}:#{name}"
90
+ "#{@type}:#{name}"
107
91
  end
108
92
 
109
93
  private
110
94
 
111
- def wait_until!(seconds: nil, time: nil)
95
+ def wait_until!(context, seconds: nil, time: nil)
112
96
  context.state["WaitUntil"] =
113
97
  if seconds
114
98
  (Time.parse(context.state["EnteredTime"]) + seconds).iso8601
@@ -9,7 +9,7 @@ module Floe
9
9
  def initialize(workflow, name, payload)
10
10
  super
11
11
 
12
- validate_state!
12
+ validate_state!(workflow)
13
13
 
14
14
  @choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
15
15
  @default = payload["Default"]
@@ -18,7 +18,7 @@ module Floe
18
18
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
19
19
  end
20
20
 
21
- def finish
21
+ def finish(context)
22
22
  output = output_path.value(context, context.input)
23
23
  next_state = choices.detect { |choice| choice.true?(context, output) }&.next || default
24
24
 
@@ -27,7 +27,7 @@ module Floe
27
27
  super
28
28
  end
29
29
 
30
- def running?
30
+ def running?(_)
31
31
  false
32
32
  end
33
33
 
@@ -37,9 +37,9 @@ module Floe
37
37
 
38
38
  private
39
39
 
40
- def validate_state!
40
+ def validate_state!(workflow)
41
41
  validate_state_choices!
42
- validate_state_default!
42
+ validate_state_default!(workflow)
43
43
  end
44
44
 
45
45
  def validate_state_choices!
@@ -47,7 +47,7 @@ module Floe
47
47
  raise Floe::InvalidWorkflowError, "\"Choices\" must be a non-empty array" unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
48
48
  end
49
49
 
50
- def validate_state_default!
50
+ def validate_state_default!(workflow)
51
51
  raise Floe::InvalidWorkflowError, "\"Default\" not in \"States\"" unless workflow.payload["States"].include?(payload["Default"])
52
52
  end
53
53
  end
@@ -15,7 +15,7 @@ module Floe
15
15
  @error_path = Path.new(payload["ErrorPath"]) if payload["ErrorPath"]
16
16
  end
17
17
 
18
- def finish
18
+ def finish(context)
19
19
  context.next_state = nil
20
20
  # TODO: support intrinsic functions here
21
21
  # see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html
@@ -27,7 +27,7 @@ module Floe
27
27
  super
28
28
  end
29
29
 
30
- def running?
30
+ def running?(_)
31
31
  false
32
32
  end
33
33
 
@@ -4,23 +4,23 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  module InputOutputMixin
7
- def process_input(input)
8
- input = input_path.value(context, input)
7
+ def process_input(context)
8
+ input = input_path.value(context, context.input)
9
9
  input = parameters.value(context, input) if parameters
10
10
  input
11
11
  end
12
12
 
13
- def process_output(input, results)
14
- return input if results.nil?
13
+ def process_output(context, results)
14
+ return context.input.dup if results.nil?
15
15
  return if output_path.nil?
16
16
 
17
17
  results = result_selector.value(context, results) if @result_selector
18
18
  if result_path.payload.start_with?("$.Credentials")
19
- credentials = result_path.set(workflow.credentials, results)["Credentials"]
20
- workflow.credentials.merge!(credentials)
21
- output = input
19
+ credentials = result_path.set(context.credentials, results)["Credentials"]
20
+ context.credentials.merge!(credentials)
21
+ output = context.input.dup
22
22
  else
23
- output = result_path.set(input, results)
23
+ output = result_path.set(context.input.dup, results)
24
24
  end
25
25
 
26
26
  output_path.value(context, output)
@@ -4,14 +4,14 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  module NonTerminalMixin
7
- def finish
8
- # If this state is failed or the End state set next_state to nil
9
- context.next_state = end? || context.failed? ? nil : @next
7
+ def finish(context)
8
+ # If this state is failed or this is an end state, next_state to nil
9
+ context.next_state ||= end? || context.failed? ? nil : @next
10
10
 
11
11
  super
12
12
  end
13
13
 
14
- def validate_state_next!
14
+ def validate_state_next!(workflow)
15
15
  raise Floe::InvalidWorkflowError, "Missing \"Next\" field in state [#{name}]" if @next.nil? && !@end
16
16
  raise Floe::InvalidWorkflowError, "\"Next\" [#{@next}] not in \"States\" for state [#{name}]" if @next && !workflow.payload["States"].key?(@next)
17
17
  end
@@ -21,15 +21,15 @@ module Floe
21
21
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
22
22
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
23
23
 
24
- validate_state!
24
+ validate_state!(workflow)
25
25
  end
26
26
 
27
- def finish
28
- context.output = process_output(context.input.dup, result)
27
+ def finish(context)
28
+ context.output = process_output(context, result)
29
29
  super
30
30
  end
31
31
 
32
- def running?
32
+ def running?(_)
33
33
  false
34
34
  end
35
35
 
@@ -39,8 +39,8 @@ module Floe
39
39
 
40
40
  private
41
41
 
42
- def validate_state!
43
- validate_state_next!
42
+ def validate_state!(workflow)
43
+ validate_state_next!(workflow)
44
44
  end
45
45
  end
46
46
  end
@@ -6,13 +6,13 @@ module Floe
6
6
  class Succeed < Floe::Workflow::State
7
7
  attr_reader :input_path, :output_path
8
8
 
9
- def finish
9
+ def finish(context)
10
10
  context.next_state = nil
11
11
  context.output = context.input
12
12
  super
13
13
  end
14
14
 
15
- def running?
15
+ def running?(_)
16
16
  false
17
17
  end
18
18
 
@@ -29,36 +29,37 @@ module Floe
29
29
  @result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
30
30
  @credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
31
31
 
32
- validate_state!
32
+ validate_state!(workflow)
33
+ rescue ArgumentError => err
34
+ raise Floe::InvalidWorkflowError, err.message
33
35
  end
34
36
 
35
- def start(input)
37
+ def start(context)
36
38
  super
37
39
 
38
- input = process_input(input)
39
- runner_context = runner.run_async!(resource, input, credentials&.value({}, workflow.credentials), context)
40
+ input = process_input(context)
41
+ runner_context = runner.run_async!(resource, input, credentials&.value({}, context.credentials), context)
40
42
 
41
43
  context.state["RunnerContext"] = runner_context
42
44
  end
43
45
 
44
- def finish
46
+ def finish(context)
45
47
  output = runner.output(context.state["RunnerContext"])
46
48
 
47
- if success?
49
+ if success?(context)
48
50
  output = parse_output(output)
49
- context.output = process_output(context.input.dup, output)
50
- super
51
+ context.output = process_output(context, output)
51
52
  else
52
- context.output = error = parse_error(output)
53
- super
54
- retry_state!(error) || catch_error!(error) || fail_workflow!(error)
53
+ error = parse_error(output)
54
+ retry_state!(context, error) || catch_error!(context, error) || fail_workflow!(context, error)
55
55
  end
56
+ super
56
57
  ensure
57
58
  runner.cleanup(context.state["RunnerContext"])
58
59
  end
59
60
 
60
- def running?
61
- return true if waiting?
61
+ def running?(context)
62
+ return true if waiting?(context)
62
63
 
63
64
  runner.status!(context.state["RunnerContext"])
64
65
  runner.running?(context.state["RunnerContext"])
@@ -72,11 +73,11 @@ module Floe
72
73
 
73
74
  attr_reader :runner
74
75
 
75
- def validate_state!
76
- validate_state_next!
76
+ def validate_state!(workflow)
77
+ validate_state_next!(workflow)
77
78
  end
78
79
 
79
- def success?
80
+ def success?(context)
80
81
  runner.success?(context.state["RunnerContext"])
81
82
  end
82
83
 
@@ -88,7 +89,7 @@ module Floe
88
89
  self.catch.detect { |c| (c.error_equals & [error, "States.ALL"]).any? }
89
90
  end
90
91
 
91
- def retry_state!(error)
92
+ def retry_state!(context, error)
92
93
  retrier = find_retrier(error["Error"]) if error
93
94
  return if retrier.nil?
94
95
 
@@ -102,13 +103,14 @@ module Floe
102
103
 
103
104
  return if context["State"]["RetryCount"] > retrier.max_attempts
104
105
 
105
- wait_until!(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
106
+ wait_until!(context, :seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
106
107
  context.next_state = context.state_name
107
- logger.info("Running state: [#{long_name}] with input [#{context.input}]...Retry - delay: #{wait_until}")
108
+ context.output = error
109
+ logger.info("Running state: [#{long_name}] with input [#{context.input}] got error[#{context.output}]...Retry - delay: #{wait_until(context)}")
108
110
  true
109
111
  end
110
112
 
111
- def catch_error!(error)
113
+ def catch_error!(context, error)
112
114
  catcher = find_catcher(error["Error"]) if error
113
115
  return if catcher.nil?
114
116
 
@@ -119,9 +121,11 @@ module Floe
119
121
  true
120
122
  end
121
123
 
122
- def fail_workflow!(error)
123
- context.next_state = nil
124
- context.output = {"Error" => error["Error"], "Cause" => error["Cause"]}.compact
124
+ def fail_workflow!(context, error)
125
+ # next_state is nil, and will be set to nil again in super
126
+ # keeping in here for completeness
127
+ context.next_state = nil
128
+ context.output = error
125
129
  logger.error("Running state: [#{long_name}] with input [#{context.input}]...Complete workflow - output: [#{context.output}]")
126
130
  end
127
131
 
@@ -23,28 +23,29 @@ module Floe
23
23
  @input_path = Path.new(payload.fetch("InputPath", "$"))
24
24
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
25
25
 
26
- validate_state!
26
+ validate_state!(workflow)
27
27
  end
28
28
 
29
- def start(input)
29
+ def start(context)
30
30
  super
31
31
 
32
32
  input = input_path.value(context, context.input)
33
33
 
34
34
  wait_until!(
35
+ context,
35
36
  :seconds => seconds_path ? seconds_path.value(context, input).to_i : seconds,
36
37
  :time => timestamp_path ? timestamp_path.value(context, input) : timestamp
37
38
  )
38
39
  end
39
40
 
40
- def finish
41
+ def finish(context)
41
42
  input = input_path.value(context, context.input)
42
43
  context.output = output_path.value(context, input)
43
44
  super
44
45
  end
45
46
 
46
- def running?
47
- waiting?
47
+ def running?(context)
48
+ waiting?(context)
48
49
  end
49
50
 
50
51
  def end?
@@ -53,8 +54,8 @@ module Floe
53
54
 
54
55
  private
55
56
 
56
- def validate_state!
57
- validate_state_next!
57
+ def validate_state!(workflow)
58
+ validate_state_next!(workflow)
58
59
  end
59
60
  end
60
61
  end
data/lib/floe/workflow.rb CHANGED
@@ -85,13 +85,17 @@ module Floe
85
85
  end
86
86
  end
87
87
 
88
- attr_reader :context, :credentials, :payload, :states, :states_by_name, :start_at, :name, :comment
88
+ attr_reader :context, :payload, :states, :states_by_name, :start_at, :name, :comment
89
89
 
90
- def initialize(payload, context = nil, credentials = {}, name = nil)
90
+ def initialize(payload, context = nil, credentials = nil, name = nil)
91
91
  payload = JSON.parse(payload) if payload.kind_of?(String)
92
92
  credentials = JSON.parse(credentials) if credentials.kind_of?(String)
93
93
  context = Context.new(context) unless context.kind_of?(Context)
94
94
 
95
+ # backwards compatibility
96
+ # caller should really put credentials into context and not pass that variable
97
+ context.credentials = credentials if credentials
98
+
95
99
  raise Floe::InvalidWorkflowError, "Missing field \"States\"" if payload["States"].nil?
96
100
  raise Floe::InvalidWorkflowError, "Missing field \"StartAt\"" if payload["StartAt"].nil?
97
101
  raise Floe::InvalidWorkflowError, "\"StartAt\" not in the \"States\" field" unless payload["States"].key?(payload["StartAt"])
@@ -99,47 +103,53 @@ module Floe
99
103
  @name = name
100
104
  @payload = payload
101
105
  @context = context
102
- @credentials = credentials || {}
103
106
  @comment = payload["Comment"]
104
107
  @start_at = payload["StartAt"]
105
108
 
106
109
  @states = payload["States"].to_a.map { |state_name, state| State.build!(self, state_name, state) }
107
110
  @states_by_name = @states.each_with_object({}) { |state, result| result[state.name] = state }
108
-
109
- unless context.state.key?("Name")
110
- context.state["Name"] = start_at
111
- context.state["Input"] = context.execution["Input"].dup
112
- end
113
- rescue StandardError => err
111
+ rescue Floe::InvalidWorkflowError
112
+ raise
113
+ rescue => err
114
114
  raise Floe::InvalidWorkflowError, err.message
115
115
  end
116
116
 
117
117
  def run_nonblock
118
+ start_workflow
118
119
  loop while step_nonblock == 0 && !end?
119
120
  self
120
121
  end
121
122
 
123
+ # NOTE: If running manually, make sure to call start_workflow at startup
122
124
  def step_nonblock
123
125
  return Errno::EPERM if end?
124
126
 
125
- step_next
126
- current_state.run_nonblock!
127
+ result = current_state.run_nonblock!(context)
128
+ return result if result != 0
129
+
130
+ # if it completed the step
131
+ context.state_history << context.state
132
+ context.next_state ? step! : end_workflow!
133
+
134
+ result
127
135
  end
128
136
 
137
+ # if this hasn't started (and we have no current_state yet), assume it is ready
129
138
  def step_nonblock_wait(timeout: nil)
130
- current_state.wait(:timeout => timeout)
139
+ context.started? ? current_state.wait(context, :timeout => timeout) : 0
131
140
  end
132
141
 
142
+ # if this hasn't started (and we have no current_state yet), assume it is ready
133
143
  def step_nonblock_ready?
134
- current_state.ready?
144
+ !context.started? || current_state.ready?(context)
135
145
  end
136
146
 
137
147
  def waiting?
138
- current_state.waiting?
148
+ current_state.waiting?(context)
139
149
  end
140
150
 
141
151
  def wait_until
142
- current_state.wait_until
152
+ current_state.wait_until(context)
143
153
  end
144
154
 
145
155
  def status
@@ -154,14 +164,46 @@ module Floe
154
164
  context.ended?
155
165
  end
156
166
 
167
+ # setup a workflow
168
+ def start_workflow
169
+ return if context.state_name
170
+
171
+ context.state["Name"] = start_at
172
+ context.state["Input"] = context.execution["Input"].dup
173
+ context.state["Guid"] = SecureRandom.uuid
174
+
175
+ context.execution["Id"] = SecureRandom.uuid
176
+ context.execution["StartTime"] = Time.now.utc.iso8601
177
+
178
+ self
179
+ end
180
+
181
+ # NOTE: Expecting the context to be initialized (via start_workflow) before this
157
182
  def current_state
158
183
  @states_by_name[context.state_name]
159
184
  end
160
185
 
186
+ # backwards compatibility. Caller should access directly from context
187
+ def credentials
188
+ @context.credentials
189
+ end
161
190
  private
162
191
 
163
- def step_next
164
- context.state = {"Name" => context.next_state, "Input" => context.output} if context.next_state
192
+ def step!
193
+ next_state = {"Name" => context.next_state, "Guid" => SecureRandom.uuid, "PreviousStateGuid" => context.state["Guid"]}
194
+
195
+ # if rerunning due to an error (and we are using Retry)
196
+ if context.state_name == context.next_state && context.failed? && context.state.key?("Retrier")
197
+ next_state.merge!(context.state.slice("RetryCount", "Input", "Retrier"))
198
+ else
199
+ next_state["Input"] = context.output
200
+ end
201
+
202
+ context.state = next_state
203
+ end
204
+
205
+ def end_workflow!
206
+ context.execution["EndTime"] = context.state["FinishedTime"]
165
207
  end
166
208
  end
167
209
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.11.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-24 00:00:00.000000000 Z
11
+ date: 2024-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn
@@ -164,13 +164,14 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- description: Simple Workflow Runner.
167
+ description: Floe is a runner for Amazon States Language workflows.
168
168
  email:
169
169
  executables:
170
170
  - floe
171
171
  extensions: []
172
172
  extra_rdoc_files: []
173
173
  files:
174
+ - ".codeclimate.yml"
174
175
  - ".rspec"
175
176
  - ".rubocop.yml"
176
177
  - ".rubocop_cc.yml"
@@ -178,6 +179,7 @@ files:
178
179
  - ".yamllint"
179
180
  - CHANGELOG.md
180
181
  - Gemfile
182
+ - LICENSE.txt
181
183
  - README.md
182
184
  - Rakefile
183
185
  - examples/set-credential.asl
@@ -185,6 +187,7 @@ files:
185
187
  - exe/floe
186
188
  - floe.gemspec
187
189
  - lib/floe.rb
190
+ - lib/floe/cli.rb
188
191
  - lib/floe/container_runner.rb
189
192
  - lib/floe/container_runner/docker.rb
190
193
  - lib/floe/container_runner/docker_mixin.rb
@@ -220,7 +223,8 @@ files:
220
223
  - renovate.json
221
224
  - sig/floe.rbs/floe.rbs
222
225
  homepage: https://github.com/ManageIQ/floe
223
- licenses: []
226
+ licenses:
227
+ - Apache-2.0
224
228
  metadata:
225
229
  allowed_push_host: https://rubygems.org
226
230
  rubygems_mfa_required: 'true'
@@ -242,8 +246,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
246
  - !ruby/object:Gem::Version
243
247
  version: '0'
244
248
  requirements: []
245
- rubygems_version: 3.5.10
249
+ rubygems_version: 3.4.20
246
250
  signing_key:
247
251
  specification_version: 4
248
- summary: Simple Workflow Runner.
252
+ summary: Floe is a runner for Amazon States Language workflows.
249
253
  test_files: []