floe 0.0.1 → 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 12212f802a614d03428c29b36a882efc7b12a2ae0e61ec1ac118aea28c34d95b
4
+ data.tar.gz: 5d62f266c83af21dffce7f8c2a1a8a3686ad24f85ad5421bced3cd483903ccb8
5
+ SHA512:
6
+ metadata.gz: 16f88b2b6f1cbb7baa4038f5c6f55da0a1f74b2a1a54b49e74ac24480dddb0b64d59fda3c6b26512a116d8060be3a171a68f6f52f4a9e4ff9a971737312c327c
7
+ data.tar.gz: b5a318594423446d5de524950a894bbcac334e5875cc2d52cdfcfaef08f068eb946a1a9120c1626392917a8a325944652b4a8d8fb21aa270393df25a3460fac7
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.1] - 2023-06-05
8
+ ### Fixed
9
+ - Fix States::Wait Path initializer arguments (#47)
10
+
11
+ ## [0.1.0] - 2023-03-13
12
+ ### Added
13
+ - Initial release
14
+
15
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.1.0...HEAD
data/Gemfile CHANGED
@@ -1,4 +1,11 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ plugin "bundler-inject", "~> 2.0"
6
+ require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil
2
7
 
3
8
  # Specify your gem's dependencies in floe.gemspec
4
9
  gemspec
10
+
11
+ gem "rake", "~> 13.0"
data/README.md CHANGED
@@ -1,29 +1,48 @@
1
1
  # Floe
2
2
 
3
- TODO: Write a gem description
3
+ [![CI](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml/badge.svg)](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml)
4
+ [![Code Climate](https://codeclimate.com/github/ManageIQ/floe.svg)](https://codeclimate.com/github/ManageIQ/floe)
5
+ [![Test Coverage](https://codeclimate.com/github/ManageIQ/floe/badges/coverage.svg)](https://codeclimate.com/github/ManageIQ/floe/coverage)
4
6
 
5
- ## Installation
7
+ ## Overview
6
8
 
7
- Add this line to your application's Gemfile:
9
+ Floe is a runner for [Amazon States Language](https://states-language.net/) workflows with support for Docker resources and running on Docker, Podman, or Kubernetes.
8
10
 
9
- gem 'floe'
11
+ ## Installation
10
12
 
11
- And then execute:
13
+ Install the gem and add to the application's Gemfile by executing:
12
14
 
13
- $ bundle
15
+ $ bundle add floe
14
16
 
15
- Or install it yourself as:
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
18
 
17
19
  $ gem install floe
18
20
 
19
21
  ## Usage
20
22
 
21
- TODO: Write usage instructions here
23
+ Floe can be run as a command-line utility or as a ruby class.
24
+
25
+ ### Command Line
26
+
27
+ ```
28
+ bundle exec ruby exe/floe --workflow examples/workflow.asl --inputs='{"foo": 1}'
29
+ ```
30
+
31
+ ### Ruby Library
32
+
33
+ ```ruby
34
+ require 'floe'
35
+
36
+ workflow = Floe::Workflow.load(File.read("workflow.asl"))
37
+ workflow.run!
38
+ ```
39
+
40
+ ## Development
41
+
42
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
43
+
44
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
22
45
 
23
46
  ## Contributing
24
47
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
48
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ManageIQ/floe.
data/Rakefile CHANGED
@@ -1 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,95 @@
1
+ {
2
+ "Comment": "An example of the Amazon States Language using a choice state.",
3
+ "StartAt": "FirstState",
4
+ "States": {
5
+ "FirstState": {
6
+ "Type": "Task",
7
+ "Resource": "docker://agrare/hello-world:latest",
8
+ "Credentials": {
9
+ "mysecret": "dont tell anyone"
10
+ },
11
+ "Retry": [
12
+ {
13
+ "ErrorEquals": [ "States.Timeout" ],
14
+ "IntervalSeconds": 3,
15
+ "MaxAttempts": 2,
16
+ "BackoffRate": 1.5
17
+ }
18
+ ],
19
+ "Catch": [
20
+ {
21
+ "ErrorEquals": [ "States.ALL" ],
22
+ "Next": "FailState"
23
+ }
24
+ ],
25
+ "Next": "ChoiceState"
26
+ },
27
+
28
+ "ChoiceState": {
29
+ "Type" : "Choice",
30
+ "Choices": [
31
+ {
32
+ "Variable": "$.foo",
33
+ "NumericEquals": 1,
34
+ "Next": "FirstMatchState"
35
+ },
36
+ {
37
+ "Variable": "$.foo",
38
+ "NumericEquals": 2,
39
+ "Next": "SecondMatchState"
40
+ },
41
+ {
42
+ "Variable": "$.foo",
43
+ "NumericEquals": 3,
44
+ "Next": "SuccessState"
45
+ }
46
+ ],
47
+ "Default": "FailState"
48
+ },
49
+
50
+ "FirstMatchState": {
51
+ "Type" : "Task",
52
+ "Resource": "docker://agrare/hello-world:latest",
53
+ "Next": "PassState"
54
+ },
55
+
56
+ "SecondMatchState": {
57
+ "Type" : "Task",
58
+ "Resource": "docker://agrare/hello-world:latest",
59
+ "Next": "NextState"
60
+ },
61
+
62
+ "PassState": {
63
+ "Type": "Pass",
64
+ "Result": {
65
+ "foo": "bar",
66
+ "bar": "baz"
67
+ },
68
+ "ResultPath": "$.result",
69
+ "Next": "WaitState"
70
+ },
71
+
72
+ "WaitState": {
73
+ "Type": "Wait",
74
+ "Seconds": 1,
75
+ "Next": "NextState"
76
+ },
77
+
78
+ "FailState": {
79
+ "Type": "Fail",
80
+ "Error": "FailStateError",
81
+ "Cause": "No Matches!"
82
+ },
83
+
84
+ "SuccessState": {
85
+ "Type": "Succeed"
86
+ },
87
+
88
+ "NextState": {
89
+ "Type": "Task",
90
+ "Resource": "docker://agrare/hello-world:latest",
91
+ "Secrets": ["vmdb:aaa-bbb-ccc"],
92
+ "End": true
93
+ }
94
+ }
95
+ }
data/exe/floe ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "floe"
5
+ require "optimist"
6
+
7
+ opts = Optimist.options do
8
+ version "v#{Floe::VERSION}\n"
9
+ opt :workflow, "Path to your workflow json", :type => :string, :required => true
10
+ opt :inputs, "JSON payload to input to the workflow", :type => :string, :default => '{}'
11
+ opt :credentials, "JSON payload with credentials", :type => :string, :default => '{}'
12
+ end
13
+
14
+ require "logger"
15
+ Floe.logger = Logger.new(STDOUT)
16
+
17
+ workflow = Floe::Workflow.load(opts[:workflow], opts[:inputs], opts[:credentials])
18
+
19
+ output = workflow.run!
20
+
21
+ puts output.inspect
data/floe.gemspec CHANGED
@@ -1,23 +1,39 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'floe/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/floe/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "floe"
8
- spec.version = Floe::VERSION
9
- spec.authors = ["Kyle Maxwell"]
10
- spec.email = ["kyle@kylemaxwell.com"]
11
- spec.description = %q{The command-line issues client}
12
- spec.summary = %q{A quirky github issues client}
13
- spec.homepage = ""
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
6
+ spec.name = "floe"
7
+ spec.version = Floe::VERSION
8
+ spec.authors = ["ManageIQ Developers"]
9
+
10
+ spec.summary = "Simple Workflow Runner."
11
+ spec.description = "Simple Workflow Runner."
12
+ spec.homepage = "https://github.com/ManageIQ/floe"
13
+ spec.required_ruby_version = ">= 2.7.0"
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
19
30
  spec.require_paths = ["lib"]
20
31
 
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
32
+ spec.add_dependency "awesome_spawn", "~>1.0"
33
+ spec.add_dependency "jsonpath", "~>1.1"
34
+ spec.add_dependency "optimist", "~>3.0"
35
+ spec.add_dependency "more_core_extensions"
36
+
37
+ spec.add_development_dependency "rubocop"
38
+ spec.add_development_dependency "rspec"
23
39
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ module Logging
5
+ def self.included(base)
6
+ base.extend(self)
7
+ end
8
+
9
+ def logger
10
+ Floe.logger
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Floe
6
+ class NullLogger < Logger
7
+ def initialize(*_args)
8
+ end
9
+
10
+ def add(*_args, &_block)
11
+ end
12
+ end
13
+ end
data/lib/floe/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Floe
2
- VERSION = "0.0.1"
4
+ VERSION = "0.1.1"
3
5
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class Catcher
6
+ attr_reader :error_equals, :next, :result_path
7
+
8
+ def initialize(payload)
9
+ @payload = payload
10
+
11
+ @error_equals = payload["ErrorEquals"]
12
+ @next = payload["Next"]
13
+ @result_path = payload.fetch("ResultPath", "$")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ChoiceRule
6
+ class Boolean < Floe::Workflow::ChoiceRule
7
+ def true?(context, input)
8
+ if payload.key?("Not")
9
+ !ChoiceRule.true?(payload["Not"], context, input)
10
+ elsif payload.key?("And")
11
+ payload["And"].all? { |choice| ChoiceRule.true?(choice, context, input) }
12
+ else
13
+ payload["Or"].any? { |choice| ChoiceRule.true?(choice, context, input) }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ChoiceRule
6
+ class Data < Floe::Workflow::ChoiceRule
7
+ def true?(context, input)
8
+
9
+ lhs = variable_value(context, input)
10
+ rhs = compare_value(context, input)
11
+
12
+ validate!(lhs)
13
+
14
+ case compare_key
15
+ when "IsNull"; is_null?(lhs)
16
+ when "IsPresent"; is_present?(lhs)
17
+ when "IsNumeric"; is_numeric?(lhs)
18
+ when "IsString"; is_string?(lhs)
19
+ when "IsBoolean"; is_boolean?(lhs)
20
+ when "IsTimestamp"; is_timestamp?(lhs)
21
+ when "StringEquals", "StringEqualsPath",
22
+ "NumericEquals", "NumericEqualsPath",
23
+ "BooleanEquals", "BooleanEqualsPath",
24
+ "TimestampEquals", "TimestampEqualsPath"
25
+ lhs == rhs
26
+ when "StringLessThan", "StringLessThanPath",
27
+ "NumericLessThan", "NumericLessThanPath",
28
+ "TimestampLessThan", "TimestampLessThanPath"
29
+ lhs < rhs
30
+ when "StringGreaterThan", "StringGreaterThanPath",
31
+ "NumericGreaterThan", "NumericGreaterThanPath",
32
+ "TimestampGreaterThan", "TimestampGreaterThanPath"
33
+ lhs > rhs
34
+ when "StringLessThanEquals", "StringLessThanEqualsPath",
35
+ "NumericLessThanEquals", "NumericLessThanEqualsPath",
36
+ "TimestampLessThanEquals", "TimestampLessThanEqualsPath"
37
+ lhs <= rhs
38
+ when "StringGreaterThanEquals", "StringGreaterThanEqualsPath",
39
+ "NumericGreaterThanEquals", "NumericGreaterThanEqualsPath",
40
+ "TimestampGreaterThanEquals", "TimestampGreaterThanEqualsPath"
41
+ lhs >= rhs
42
+ when "StringMatches"
43
+ lhs.match?(Regexp.escape(rhs).gsub('\*','.*?'))
44
+ else
45
+ raise Floe::InvalidWorkflowError, "Invalid choice [#{compare_key}]"
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def validate!(value)
52
+ raise RuntimeError, "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
53
+ end
54
+
55
+ def is_null?(value)
56
+ value.nil?
57
+ end
58
+
59
+ def is_present?(value)
60
+ !value.nil?
61
+ end
62
+
63
+ def is_numeric?(value)
64
+ value.kind_of?(Integer) || value.kind_of?(Float)
65
+ end
66
+
67
+ def is_string?(value)
68
+ value.kind_of?(String)
69
+ end
70
+
71
+ def is_boolean?(value)
72
+ [true, false].include?(value)
73
+ end
74
+
75
+ def is_timestamp?(value)
76
+ require "date"
77
+
78
+ DateTime.rfc3339(value)
79
+ true
80
+ rescue TypeError, Date::Error
81
+ false
82
+ end
83
+
84
+ def compare_key
85
+ @compare_key ||= payload.keys.detect { |key| key.match?(/^(IsNull|IsPresent|IsNumeric|IsString|IsBoolean|IsTimestamp|String|Numeric|Boolean|Timestamp)/) }
86
+ end
87
+
88
+ def compare_value(context, input)
89
+ compare_key.end_with?("Path") ? Path.value(payload[compare_key], context, input) : payload[compare_key]
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ChoiceRule
6
+ class << self
7
+ def true?(payload, context, input)
8
+ build(payload).true?(context, input)
9
+ end
10
+
11
+ def build(payload)
12
+ data_expression = (payload.keys & %w[And Not Or]).empty?
13
+ if data_expression
14
+ Floe::Workflow::ChoiceRule::Data.new(payload)
15
+ else
16
+ Floe::Workflow::ChoiceRule::Boolean.new(payload)
17
+ end
18
+ end
19
+ end
20
+
21
+ attr_reader :next, :payload, :variable
22
+
23
+ def initialize(payload)
24
+ @payload = payload
25
+
26
+ @next = payload["Next"]
27
+ @variable = payload["Variable"]
28
+ end
29
+
30
+ def true?(*)
31
+ raise NotImplementedError, "Must be implemented in a subclass"
32
+ end
33
+
34
+ private
35
+
36
+ def variable_value(context, input)
37
+ @variable_value ||= Path.value(variable, context, input)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class Path
6
+ class << self
7
+ def value(payload, context, input = {})
8
+ new(payload).value(context, input)
9
+ end
10
+ end
11
+
12
+ def initialize(payload)
13
+ @payload = payload
14
+ end
15
+
16
+ def value(context, input = {})
17
+ obj, path =
18
+ if payload.start_with?("$$")
19
+ [context, payload[1..]]
20
+ else
21
+ [input, payload]
22
+ end
23
+
24
+ results = JsonPath.on(obj, path)
25
+
26
+ results.count < 2 ? results.first : results
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :payload
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class PayloadTemplate
6
+ def initialize(payload)
7
+ @payload = payload
8
+ end
9
+
10
+ def value(context, inputs = {})
11
+ interpolate_value_nested(payload, context, inputs)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :payload
17
+
18
+ def interpolate_value_nested(value, context, inputs)
19
+ case value
20
+ when Array
21
+ value.map { |val| interpolate_value_nested(val, context, inputs) }
22
+ when Hash
23
+ value.to_h do |key, val|
24
+ val = interpolate_value_nested(val, context, inputs)
25
+ key = key.gsub(/\.\$$/, "") if key.end_with?(".$")
26
+
27
+ [key, val]
28
+ end
29
+ when String
30
+ value.start_with?("$") ? Path.value(value, context, inputs) : value
31
+ else
32
+ value
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ReferencePath < Path
6
+ class << self
7
+ def set (payload, context, value)
8
+ new(payload).set(context, value)
9
+ end
10
+ end
11
+
12
+ def initialize(*)
13
+ require "more_core_extensions/core_ext/hash/nested"
14
+ require "more_core_extensions/core_ext/array/nested"
15
+
16
+ super
17
+
18
+ raise Floe::InvalidWorkflowError, "Invalid Reference Path" if payload.match?(/@|,|:|\?/)
19
+ end
20
+
21
+ def set(context, value)
22
+ result = context.dup
23
+
24
+ path = JsonPath.new(payload)
25
+ .path[1..]
26
+ .map { |v| v.match(/\[(?<name>.+)\]/)["name"] }
27
+ .map { |v| v[0] == "'" ? v.gsub("'", "") : v.to_i }
28
+ .compact
29
+
30
+ # If the payload is '$' then merge the value into the context
31
+ # otherwise use store path to set the value to a sub-key
32
+ #
33
+ # TODO: how to handle non-hash values, raise error if path=$ and value not a hash?
34
+ if path.empty?
35
+ result.merge!(value)
36
+ else
37
+ result.store_path(path, value)
38
+ end
39
+
40
+ result
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class Retrier
6
+ attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate
7
+
8
+ def initialize(payload)
9
+ @payload = payload
10
+
11
+ @error_equals = payload["ErrorEquals"]
12
+ @interval_seconds = payload["IntervalSeconds"] || 1.0
13
+ @max_attempts = payload["MaxAttempts"] || 3
14
+ @backoff_rate = payload["BackoffRate"] || 2.0
15
+ end
16
+
17
+ def sleep_duration(attempt)
18
+ interval_seconds * (backoff_rate * attempt)
19
+ end
20
+ end
21
+ end
22
+ end