floe 0.11.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb1a786256a191ba7d1b3b08f6e3cd02fa778fcf28ab44a7353f36ae7979039b
4
- data.tar.gz: ca5ab5d0b5ed1949945593bb1f07141e554fbbac4d825cbf6333bb92e5b17b6d
3
+ metadata.gz: abe15498a790293b2a375c4538ed649bfa577edd1b7eaa38d02266fc4f7fc576
4
+ data.tar.gz: 733e0e6687ca143de8a74214f13b4a78118333a99f89a9a09a9998ab0319768a
5
5
  SHA512:
6
- metadata.gz: e96574a58740f8659f27947144bdc5067fbade1645a0b651e66607494fce5b0a16ef762bbba1638d54a8804425c2de2c3938884b4c18346be3623b4ffbdc821b
7
- data.tar.gz: da673ecc866d1df2f6f75872df183beda957996fd54ff09177ebd84b27bc64fade88122eaaf34f7134e3a92d74b9d9f7a7322648042df444ad889de9a053d298
6
+ metadata.gz: d5dfc65c86e68d39b7c9bbeedd20e2a3ea844f158d601bb72bf35595e6d6597ac6571f7bc9ad6802f8ed33d6753b4906e7745a3e46cb38790492869aa95e6a02
7
+ data.tar.gz: 913032cb3a042e8b3464f05c7e0e65401f8532b28ded1cebd57df310899dcc8bd1bc26d9307a6b824837474c0e3ee9144477147779e0b40b6fd9c9fecfc7bddc
data/.yamllint CHANGED
@@ -1,8 +1,6 @@
1
- ---
2
1
  extends: relaxed
3
-
4
2
  rules:
5
3
  indentation:
6
4
  indent-sequences: false
7
5
  line-length:
8
- max: 120
6
+ max: 1000
data/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.0] - 2024-08-12
8
+ ### Added
9
+ - Choice rule payload validation path ([#253](https://github.com/ManageIQ/floe/pull/253))
10
+ - Intrinsics JsonToString and StringToJson ([#256](https://github.com/ManageIQ/floe/pull/256))
11
+ - Add States.Format intrinsic function ([#258](https://github.com/ManageIQ/floe/pull/258))
12
+ - Intrinsics States.JsonMerge ([#255](https://github.com/ManageIQ/floe/pull/255))
13
+ - Enable support for Hashes in States.Hash ([#260](https://github.com/ManageIQ/floe/pull/260))
14
+
15
+ ## [0.12.0] - 2024-07-31
16
+ ### Added
17
+ - Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
18
+ - Add InstrinsicFunction foundation + States.Array, States.UUID ([#194](https://github.com/ManageIQ/floe/pull/194))
19
+ - Evaluate IntrinsicFunctions from PayloadTemplate ([#236](https://github.com/ManageIQ/floe/pull/236))
20
+ - Add more intrinsic functions ([#242](https://github.com/ManageIQ/floe/pull/242))
21
+ - Add State#cause_path, error_path ([#249](https://github.com/ManageIQ/floe/pull/249))
22
+ - Validate Catcher Next ([#250](https://github.com/ManageIQ/floe/pull/250))
23
+ - Add spec/supports ([#248](https://github.com/ManageIQ/floe/pull/248))
24
+
25
+ ### Fixed
26
+ - Handle non-hash input/output values ([#214](https://github.com/ManageIQ/floe/pull/214))
27
+ - Fix Input/Output handling for Pass, Choice, Succeed states ([#225](https://github.com/ManageIQ/floe/pull/225))
28
+ - Wrap Parslet::ParseFailed errors as Floe::InvalidWorkflowError ([#235](https://github.com/ManageIQ/floe/pull/235))
29
+ - Fix edge cases with States.Array ([#237](https://github.com/ManageIQ/floe/pull/237))
30
+ - Fix sporadic test failure with Wait state ([#243](https://github.com/ManageIQ/floe/pull/243))
31
+ - Fix invalid container names after normalizing ([#252](https://github.com/ManageIQ/floe/pull/252))
32
+
33
+ ### Changed
34
+ - Extract ErrorMatcherMixin from Catch and Retry ([#186](https://github.com/ManageIQ/floe/pull/186))
35
+ - Output should be JSON ([#230](https://github.com/ManageIQ/floe/pull/230))
36
+ - Normalize functions to all take arguments ([#238](https://github.com/ManageIQ/floe/pull/238))
37
+ - Pass full path name to State.new for better errors ([#229](https://github.com/ManageIQ/floe/pull/229))
38
+ - Validate that state machine input is valid JSON ([#227](https://github.com/ManageIQ/floe/pull/227))
39
+ - Move the Parslet parse into initialize so that invalid function definition will fail on workflow load ([#245](https://github.com/ManageIQ/floe/pull/245))
40
+
7
41
  ## [0.11.3] - 2024-06-20
8
42
  ### Fixed
9
43
  - ResultPath=$ replaces complete output ([#199](https://github.com/ManageIQ/floe/pull/199))
@@ -199,7 +233,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
199
233
  ### Added
200
234
  - Initial release
201
235
 
202
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.11.3...HEAD
236
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.13.0...HEAD
237
+ [0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
238
+ [0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
203
239
  [0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
204
240
  [0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
205
241
  [0.11.1]: https://github.com/ManageIQ/floe/compare/v0.11.0...v0.11.1
data/floe.gemspec CHANGED
@@ -36,11 +36,11 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "jsonpath", "~>1.1"
37
37
  spec.add_dependency "kubeclient", "~>4.7"
38
38
  spec.add_dependency "optimist", "~>3.0"
39
+ spec.add_dependency "parslet", "~>2.0"
39
40
 
40
- spec.add_development_dependency "manageiq-style"
41
+ spec.add_development_dependency "manageiq-style", ">= 1.5.2"
41
42
  spec.add_development_dependency "rake", "~> 13.0"
42
43
  spec.add_development_dependency "rspec"
43
- spec.add_development_dependency "rubocop"
44
44
  spec.add_development_dependency "simplecov", ">= 0.21.2"
45
45
  spec.add_development_dependency "timecop"
46
46
  end
data/lib/floe/cli.rb CHANGED
@@ -7,6 +7,7 @@ module Floe
7
7
  require "logger"
8
8
 
9
9
  Floe.logger = Logger.new($stdout)
10
+ Floe.logger.level = 0 if ENV["DEBUG"]
10
11
  end
11
12
 
12
13
  def run(args = ARGV)
@@ -30,10 +31,12 @@ module Floe
30
31
  # Display status
31
32
  workflows.each do |workflow|
32
33
  puts "", "#{workflow.name}#{" (#{workflow.status})" unless workflow.context.success?}", "===" if workflows.size > 1
33
- puts workflow.output.inspect
34
+ puts workflow.output
34
35
  end
35
36
 
36
37
  workflows.all? { |workflow| workflow.context.success? }
38
+ rescue Floe::Error => err
39
+ abort(err.message)
37
40
  end
38
41
 
39
42
  private
@@ -24,6 +24,9 @@ module Floe
24
24
  # This does not follow the leading and trailing character restriction because we will embed it
25
25
  # below with a prefix and suffix that already conform to the RFC.
26
26
  normalized_name = name.downcase.gsub(/[^a-z0-9-]/, "-")[0, MAX_CONTAINER_NAME_SIZE]
27
+ # Ensure that the normalized_name doesn't end in any invalid characters after we
28
+ # limited the length to the MAX_CONTAINER_NAME_SIZE.
29
+ normalized_name.gsub!(/[^a-z0-9]+$/, "")
27
30
 
28
31
  "floe-#{normalized_name}-#{SecureRandom.hex(4)}"
29
32
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ module ValidationMixin
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ def parser_error!(comment)
10
+ self.class.parser_error!(name, comment)
11
+ end
12
+
13
+ def missing_field_error!(field_name)
14
+ self.class.missing_field_error!(name, field_name)
15
+ end
16
+
17
+ def invalid_field_error!(field_name, field_value = nil, comment = nil)
18
+ self.class.invalid_field_error!(name, field_name, field_value, comment)
19
+ end
20
+
21
+ def workflow_state?(field_value, workflow)
22
+ workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
23
+ end
24
+
25
+ def wrap_parser_error(field_name, field_value)
26
+ yield
27
+ rescue ArgumentError, InvalidWorkflowError => error
28
+ invalid_field_error!(field_name, field_value, error.message)
29
+ end
30
+
31
+ module ClassMethods
32
+ def parser_error!(name, comment)
33
+ name = name.join(".") if name.kind_of?(Array)
34
+ raise Floe::InvalidWorkflowError, "#{name} #{comment}"
35
+ end
36
+
37
+ def missing_field_error!(name, field_name)
38
+ parser_error!(name, "does not have required field \"#{field_name}\"")
39
+ end
40
+
41
+ def invalid_field_error!(name, field_name, field_value, comment)
42
+ # instead of displaying a large hash or array, just displaying the word Hash or Array
43
+ field_value = field_value.class if field_value.kind_of?(Hash) || field_value.kind_of?(Array)
44
+
45
+ parser_error!(name, "field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}")
46
+ end
47
+ end
48
+ end
49
+ 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.3"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -3,14 +3,28 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class Catcher
6
- attr_reader :error_equals, :next, :result_path
6
+ include ErrorMatcherMixin
7
+ include ValidationMixin
7
8
 
8
- def initialize(payload)
9
- @payload = payload
9
+ attr_reader :error_equals, :next, :result_path, :name
10
+
11
+ def initialize(workflow, name, payload)
12
+ @name = name
13
+ @payload = payload
10
14
 
11
15
  @error_equals = payload["ErrorEquals"]
12
16
  @next = payload["Next"]
13
17
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
18
+
19
+ missing_field_error!("ErrorEquals") if !@error_equals.kind_of?(Array) || @error_equals.empty?
20
+ validate_state_next!(workflow)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_state_next!(workflow)
26
+ missing_field_error!("Next") if @next.nil?
27
+ invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow_state?(@next, workflow)
14
28
  end
15
29
  end
16
30
  end
@@ -5,14 +5,13 @@ module Floe
5
5
  class ChoiceRule
6
6
  class Data < Floe::Workflow::ChoiceRule
7
7
  def true?(context, input)
8
+ return presence_check(context, input) if compare_key == "IsPresent"
9
+
8
10
  lhs = variable_value(context, input)
9
11
  rhs = compare_value(context, input)
10
12
 
11
- validate!(lhs)
12
-
13
13
  case compare_key
14
14
  when "IsNull" then is_null?(lhs)
15
- when "IsPresent" then is_present?(lhs)
16
15
  when "IsNumeric" then is_numeric?(lhs)
17
16
  when "IsString" then is_string?(lhs)
18
17
  when "IsBoolean" then is_boolean?(lhs)
@@ -47,8 +46,21 @@ module Floe
47
46
 
48
47
  private
49
48
 
50
- def validate!(value)
51
- raise "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
49
+ def presence_check(context, input)
50
+ # Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
51
+ # If true then return true when present.
52
+ # If false then return true when not present.
53
+ rhs = compare_value(context, input)
54
+ # Don't need the variable_value, just need to see if the path finds the value.
55
+ variable_value(context, input)
56
+
57
+ # The variable_value is present
58
+ # If rhs is true, then presence check was successful, return true.
59
+ rhs
60
+ rescue Floe::PathError
61
+ # variable_value is not present. (the path lookup threw an error)
62
+ # If rhs is false, then it successfully wasn't present, return true.
63
+ !rhs
52
64
  end
53
65
 
54
66
  def is_null?(value) # rubocop:disable Naming/PredicateName
@@ -4,26 +4,31 @@ module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
6
  class << self
7
- def build(payload)
7
+ def build(workflow, name, payload)
8
8
  if (sub_payloads = payload["Not"])
9
- Floe::Workflow::ChoiceRule::Not.new(payload, build_children([sub_payloads]))
9
+ name += ["Not"]
10
+ Floe::Workflow::ChoiceRule::Not.new(workflow, name, payload, build_children(workflow, name, [sub_payloads]))
10
11
  elsif (sub_payloads = payload["And"])
11
- Floe::Workflow::ChoiceRule::And.new(payload, build_children(sub_payloads))
12
+ name += ["And"]
13
+ Floe::Workflow::ChoiceRule::And.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
12
14
  elsif (sub_payloads = payload["Or"])
13
- Floe::Workflow::ChoiceRule::Or.new(payload, build_children(sub_payloads))
15
+ name += ["Or"]
16
+ Floe::Workflow::ChoiceRule::Or.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
14
17
  else
15
- Floe::Workflow::ChoiceRule::Data.new(payload)
18
+ name += ["Data"]
19
+ Floe::Workflow::ChoiceRule::Data.new(workflow, name, payload)
16
20
  end
17
21
  end
18
22
 
19
- def build_children(sub_payloads)
20
- sub_payloads.map { |payload| build(payload) }
23
+ def build_children(workflow, name, sub_payloads)
24
+ sub_payloads.map.with_index { |payload, i| build(workflow, name + [i.to_s], payload) }
21
25
  end
22
26
  end
23
27
 
24
- attr_reader :next, :payload, :variable, :children
28
+ attr_reader :next, :payload, :variable, :children, :name
25
29
 
26
- def initialize(payload, children = nil)
30
+ def initialize(_workflow, name, payload, children = nil)
31
+ @name = name
27
32
  @payload = payload
28
33
  @children = children
29
34
 
@@ -9,9 +9,7 @@ module Floe
9
9
  # @param input [Hash] (default: {})
10
10
  def initialize(context = nil, input: nil, credentials: {})
11
11
  context = JSON.parse(context) if context.kind_of?(String)
12
-
13
- input ||= {}
14
- input = JSON.parse(input) if input.kind_of?(String)
12
+ input = JSON.parse(input || "{}")
15
13
 
16
14
  @context = context || {}
17
15
  self["Execution"] ||= {}
@@ -23,7 +21,7 @@ module Floe
23
21
 
24
22
  @credentials = credentials || {}
25
23
  rescue JSON::ParserError => err
26
- raise Floe::InvalidWorkflowError, err.message
24
+ raise Floe::InvalidExecutionInput, "Invalid State Machine Execution Input: #{err}: was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"
27
25
  end
28
26
 
29
27
  def execution
@@ -39,7 +37,7 @@ module Floe
39
37
  end
40
38
 
41
39
  def failed?
42
- output&.key?("Error") || false
40
+ (output.kind_of?(Hash) && output.key?("Error")) || false
43
41
  end
44
42
 
45
43
  def ended?
@@ -54,10 +52,18 @@ module Floe
54
52
  state["Input"]
55
53
  end
56
54
 
55
+ def json_input
56
+ input.to_json
57
+ end
58
+
57
59
  def output
58
60
  state["Output"]
59
61
  end
60
62
 
63
+ def json_output
64
+ output.to_json
65
+ end
66
+
61
67
  def output=(val)
62
68
  state["Output"] = val
63
69
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ # Methods for common error handling
6
+ module ErrorMatcherMixin
7
+ # @param [String] error the error thrown
8
+ def match_error?(error)
9
+ return false if error == "States.Runtime"
10
+ return true if error_equals.include?("States.ALL")
11
+ return true if error_equals.include?("States.Timeout") && error == "States.HeartbeatTimeout"
12
+
13
+ error_equals.include?(error)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,108 @@
1
+ require "parslet"
2
+
3
+ module Floe
4
+ class Workflow
5
+ class IntrinsicFunction
6
+ class Parser < Parslet::Parser
7
+ rule(:spaces) { str(' ').repeat(1) }
8
+ rule(:spaces?) { spaces.maybe }
9
+ rule(:digit) { match('[0-9]') }
10
+ rule(:quote) { str('\'') }
11
+
12
+ rule(:comma_sep) { str(',') >> spaces? }
13
+
14
+ rule(:true_literal) { str('true').as(:true_literal) }
15
+ rule(:false_literal) { str('false').as(:false_literal) }
16
+ rule(:null_literal) { str('null').as(:null_literal) }
17
+
18
+ rule(:number) do
19
+ (
20
+ str('-').maybe >> (
21
+ str('0') | (match('[1-9]') >> digit.repeat)
22
+ ) >> (
23
+ str('.') >> digit.repeat(1)
24
+ ).maybe >> (
25
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
26
+ ).maybe
27
+ ).as(:number)
28
+ end
29
+
30
+ rule(:string) do
31
+ (
32
+ quote >> (
33
+ (str('\\') >> any) | (quote.absent? >> any)
34
+ ).repeat >> quote
35
+ ).as(:string)
36
+ end
37
+
38
+ rule(:jsonpath) do
39
+ (
40
+ str('$') >> match('[^,)]').repeat(0)
41
+ ).as(:jsonpath)
42
+ end
43
+
44
+ rule(:arg) do
45
+ (
46
+ string | number | jsonpath | true_literal | false_literal | null_literal | expression
47
+ ).as(:arg)
48
+ end
49
+
50
+ rule(:args) do
51
+ (
52
+ arg >> (comma_sep >> arg).repeat
53
+ ).maybe.as(:args)
54
+ end
55
+
56
+ [
57
+ :states_format, "States.Format",
58
+ :states_string_to_json, "States.StringToJson",
59
+ :states_json_to_string, "States.JsonToString",
60
+ :states_array, "States.Array",
61
+ :states_array_partition, "States.ArrayPartition",
62
+ :states_array_contains, "States.ArrayContains",
63
+ :states_array_range, "States.ArrayRange",
64
+ :states_array_get_item, "States.ArrayGetItem",
65
+ :states_array_length, "States.ArrayLength",
66
+ :states_array_unique, "States.ArrayUnique",
67
+ :states_base64_encode, "States.Base64Encode",
68
+ :states_base64_decode, "States.Base64Decode",
69
+ :states_hash, "States.Hash",
70
+ :states_json_merge, "States.JsonMerge",
71
+ :states_math_random, "States.MathRandom",
72
+ :states_math_add, "States.MathAdd",
73
+ :states_string_split, "States.StringSplit",
74
+ :states_uuid, "States.UUID",
75
+ ].each_slice(2) do |function_symbol, function_name|
76
+ rule(function_symbol) do
77
+ (
78
+ str(function_name) >> str('(') >> args >> str(')')
79
+ ).as(function_symbol)
80
+ end
81
+ end
82
+
83
+ rule(:expression) do
84
+ states_format |
85
+ states_string_to_json |
86
+ states_json_to_string |
87
+ states_array |
88
+ states_array_partition |
89
+ states_array_contains |
90
+ states_array_range |
91
+ states_array_get_item |
92
+ states_array_length |
93
+ states_array_unique |
94
+ states_base64_encode |
95
+ states_base64_decode |
96
+ states_hash |
97
+ states_json_merge |
98
+ states_math_random |
99
+ states_math_add |
100
+ states_string_split |
101
+ states_uuid
102
+ end
103
+
104
+ root(:expression)
105
+ end
106
+ end
107
+ end
108
+ end