floe 0.0.1 → 0.1.1

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