floe 0.11.2 → 0.11.3

Sign up to get free protection for your applications and to get access to all the features.
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: []