floe 0.11.3 → 0.13.0

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: 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