floe 0.12.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eb195983f47d7cac44f18e962bf314c3ff4fefbdf3ac5cae32e960bfc2c0fb0
4
- data.tar.gz: 6a0f8d91337297786c03e99fc96cb118ed8cb76244e52cf8253702d1517a6f5d
3
+ metadata.gz: 6fb1e855fda7d4a00a72e494b820a89304350945c14a71675d5cc6306e69f079
4
+ data.tar.gz: 9780e210073c41b4a5c534bdabd301c2df6275c13328dbb1ee43569470e8d590
5
5
  SHA512:
6
- metadata.gz: 99f5a3c8e67a9f5c97b43795739c201a49a8f1195d40a1da38f2aa0e16d200266783336e4be9ada5d41b566c545144a4533db258546ebbf04a23c454a489e6a1
7
- data.tar.gz: a77bfe436b37adf9dbdc2a279b9b1f70f48980062aaaec191a8e1fd57238e8b8b50b02828025e561c7ccb8429cb67fefb3f7ed044ed309d82eaae8dd8d9d407b
6
+ metadata.gz: bec5c7f6337c74258e185b76abfc3953f05ef16cf4de640972d23b7dfa81bf4439df3ce00dd539c618f7d18905d783bfa55dc777365e1a48969942b182448107
7
+ data.tar.gz: cf584d349c69927dec6945fe21d652b79e96f6228754b1a91a57b3241825661ed6d977a7d53e3ebfae526551da2a4a573148e9d855840d8edc52b8ebeffc948b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.1] - 2024-08-16
8
+ ### Fixed
9
+ - Fix podman/docker container_ref trailing newline ([#265](https://github.com/ManageIQ/floe/pull/265))
10
+
11
+ ### Changed
12
+ - Improve type check for States.Hash ([#261](https://github.com/ManageIQ/floe/pull/261))
13
+ - Use Numeric over Integer || Float ([#264](https://github.com/ManageIQ/floe/pull/264))
14
+ - In ChoiceRule::Data, parse compare key and variable ([#257](https://github.com/ManageIQ/floe/pull/257))
15
+
16
+ ## [0.13.0] - 2024-08-12
17
+ ### Added
18
+ - Choice rule payload validation path ([#253](https://github.com/ManageIQ/floe/pull/253))
19
+ - Intrinsics JsonToString and StringToJson ([#256](https://github.com/ManageIQ/floe/pull/256))
20
+ - Add States.Format intrinsic function ([#258](https://github.com/ManageIQ/floe/pull/258))
21
+ - Intrinsics States.JsonMerge ([#255](https://github.com/ManageIQ/floe/pull/255))
22
+ - Enable support for Hashes in States.Hash ([#260](https://github.com/ManageIQ/floe/pull/260))
23
+
7
24
  ## [0.12.0] - 2024-07-31
8
25
  ### Added
9
26
  - Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
@@ -225,7 +242,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
225
242
  ### Added
226
243
  - Initial release
227
244
 
228
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.12.0...HEAD
245
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.13.1...HEAD
246
+ [0.13.1]: https://github.com/ManageIQ/floe/compare/v0.13.0...v0.13.1
247
+ [0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
229
248
  [0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
230
249
  [0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
231
250
  [0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
@@ -129,7 +129,7 @@ module Floe
129
129
  logger.debug("Running #{AwesomeSpawn.build_command_line(self.class::DOCKER_COMMAND, params)}")
130
130
 
131
131
  result = docker!(*params)
132
- result.output
132
+ result.output.chomp
133
133
  end
134
134
 
135
135
  def run_container_params(image, env, secrets_file)
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.12.0"
4
+ VERSION = "0.13.1"
5
5
  end
@@ -4,15 +4,26 @@ module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
6
  class Data < Floe::Workflow::ChoiceRule
7
+ COMPARE_KEYS = %w[IsNull IsPresent IsNumeric IsString IsBoolean IsTimestamp String Numeric Boolean Timestamp].freeze
8
+
9
+ attr_reader :variable, :compare_key, :value, :path
10
+
11
+ def initialize(_workflow, _name, payload)
12
+ super
13
+
14
+ @variable = parse_path("Variable", payload)
15
+ parse_compare_key
16
+ @value = path ? parse_path(compare_key, payload) : payload[compare_key]
17
+ end
18
+
7
19
  def true?(context, input)
20
+ return presence_check(context, input) if compare_key == "IsPresent"
21
+
8
22
  lhs = variable_value(context, input)
9
23
  rhs = compare_value(context, input)
10
24
 
11
- validate!(lhs)
12
-
13
25
  case compare_key
14
26
  when "IsNull" then is_null?(lhs)
15
- when "IsPresent" then is_present?(lhs)
16
27
  when "IsNumeric" then is_numeric?(lhs)
17
28
  when "IsString" then is_string?(lhs)
18
29
  when "IsBoolean" then is_boolean?(lhs)
@@ -47,8 +58,21 @@ module Floe
47
58
 
48
59
  private
49
60
 
50
- def validate!(value)
51
- raise "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
61
+ def presence_check(context, input)
62
+ # Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
63
+ # If true then return true when present.
64
+ # If false then return true when not present.
65
+ rhs = compare_value(context, input)
66
+ # Don't need the variable_value, just need to see if the path finds the value.
67
+ variable_value(context, input)
68
+
69
+ # The variable_value is present
70
+ # If rhs is true, then presence check was successful, return true.
71
+ rhs
72
+ rescue Floe::PathError
73
+ # variable_value is not present. (the path lookup threw an error)
74
+ # If rhs is false, then it successfully wasn't present, return true.
75
+ !rhs
52
76
  end
53
77
 
54
78
  def is_null?(value) # rubocop:disable Naming/PredicateName
@@ -60,7 +84,7 @@ module Floe
60
84
  end
61
85
 
62
86
  def is_numeric?(value) # rubocop:disable Naming/PredicateName
63
- value.kind_of?(Integer) || value.kind_of?(Float)
87
+ value.kind_of?(Numeric)
64
88
  end
65
89
 
66
90
  def is_string?(value) # rubocop:disable Naming/PredicateName
@@ -80,12 +104,25 @@ module Floe
80
104
  false
81
105
  end
82
106
 
83
- def compare_key
84
- @compare_key ||= payload.keys.detect { |key| key.match?(/^(IsNull|IsPresent|IsNumeric|IsString|IsBoolean|IsTimestamp|String|Numeric|Boolean|Timestamp)/) }
107
+ def parse_compare_key
108
+ @compare_key = payload.keys.detect { |key| key.match?(/^(#{COMPARE_KEYS.join("|")})/) }
109
+ parser_error!("requires a compare key") unless compare_key
110
+
111
+ @path = compare_key.end_with?("Path")
85
112
  end
86
113
 
87
114
  def compare_value(context, input)
88
- compare_key.end_with?("Path") ? Path.value(payload[compare_key], context, input) : payload[compare_key]
115
+ path ? value.value(context, input) : value
116
+ end
117
+
118
+ def variable_value(context, input)
119
+ variable.value(context, input)
120
+ end
121
+
122
+ def parse_path(field_name, payload)
123
+ value = payload[field_name]
124
+ missing_field_error!(field_name) unless value
125
+ wrap_parser_error(field_name, value) { Path.new(value) }
89
126
  end
90
127
  end
91
128
  end
@@ -3,6 +3,8 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
+ include ValidationMixin
7
+
6
8
  class << self
7
9
  def build(workflow, name, payload)
8
10
  if (sub_payloads = payload["Not"])
@@ -25,15 +27,15 @@ module Floe
25
27
  end
26
28
  end
27
29
 
28
- attr_reader :next, :payload, :variable, :children, :name
30
+ attr_reader :next, :payload, :children, :name
29
31
 
30
- def initialize(_workflow, name, payload, children = nil)
32
+ def initialize(workflow, name, payload, children = nil)
31
33
  @name = name
32
34
  @payload = payload
33
35
  @children = children
36
+ @next = payload["Next"]
34
37
 
35
- @next = payload["Next"]
36
- @variable = payload["Variable"]
38
+ validate_next!(workflow)
37
39
  end
38
40
 
39
41
  def true?(*)
@@ -42,8 +44,31 @@ module Floe
42
44
 
43
45
  private
44
46
 
45
- def variable_value(context, input)
46
- Path.value(variable, context, input)
47
+ def validate_next!(workflow)
48
+ if is_child?
49
+ # non-top level nodes don't allow a next
50
+ invalid_field_error!("Next", @next, "not allowed in a child rule") if @next
51
+ elsif !@next
52
+ # top level nodes require a next
53
+ missing_field_error!("Next")
54
+ elsif !workflow_state?(@next, workflow)
55
+ # top level nodes require a next field that is found
56
+ invalid_field_error!("Next", @next, "is not found in \"States\"")
57
+ end
58
+ end
59
+
60
+ # returns true if this is a child rule underneath an And/Or/Not
61
+ # {
62
+ # "Or": [
63
+ # {"Variable": "$.foo", "IsString": true},
64
+ # {"Variable": "$.foo", "IsBoolean": true}
65
+ # ], "Next": "Finished"
66
+ # }
67
+ #
68
+ # The Or node, has no conjunction parent, so it is not a child (requires a Next)
69
+ # The 2 Data nodes have a conjunction parent, so each one is a child (do not allow a Next)
70
+ def is_child? # rubocop:disable Naming/PredicateName
71
+ !(%w[And Or Not] & name[0..-2]).empty?
47
72
  end
48
73
  end
49
74
  end
@@ -54,6 +54,9 @@ module Floe
54
54
  end
55
55
 
56
56
  [
57
+ :states_format, "States.Format",
58
+ :states_string_to_json, "States.StringToJson",
59
+ :states_json_to_string, "States.JsonToString",
57
60
  :states_array, "States.Array",
58
61
  :states_array_partition, "States.ArrayPartition",
59
62
  :states_array_contains, "States.ArrayContains",
@@ -64,6 +67,7 @@ module Floe
64
67
  :states_base64_encode, "States.Base64Encode",
65
68
  :states_base64_decode, "States.Base64Decode",
66
69
  :states_hash, "States.Hash",
70
+ :states_json_merge, "States.JsonMerge",
67
71
  :states_math_random, "States.MathRandom",
68
72
  :states_math_add, "States.MathAdd",
69
73
  :states_string_split, "States.StringSplit",
@@ -77,7 +81,10 @@ module Floe
77
81
  end
78
82
 
79
83
  rule(:expression) do
80
- states_array |
84
+ states_format |
85
+ states_string_to_json |
86
+ states_json_to_string |
87
+ states_array |
81
88
  states_array_partition |
82
89
  states_array_contains |
83
90
  states_array_range |
@@ -87,6 +94,7 @@ module Floe
87
94
  states_base64_encode |
88
95
  states_base64_decode |
89
96
  states_hash |
97
+ states_json_merge |
90
98
  states_math_random |
91
99
  states_math_add |
92
100
  states_string_split |
@@ -9,6 +9,7 @@ module Floe
9
9
  class IntrinsicFunction
10
10
  class Transformer < Parslet::Transform
11
11
  OptionalArg = Struct.new(:type)
12
+ VariadicArgs = Struct.new(:type)
12
13
 
13
14
  class << self
14
15
  def process_args(args, function, signature = nil)
@@ -37,20 +38,33 @@ module Floe
37
38
 
38
39
  def check_arity(args, function, signature)
39
40
  if signature.any?(OptionalArg)
40
- signature_without_optional = signature.reject { |a| a.kind_of?(OptionalArg) }
41
- signature_size = (signature_without_optional.size..signature.size)
41
+ signature_required = signature.reject { |a| a.kind_of?(OptionalArg) }
42
+ signature_size = (signature_required.size..signature.size)
42
43
 
43
44
  raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size)
45
+ elsif signature.any?(VariadicArgs)
46
+ signature_required = signature.reject { |a| a.kind_of?(VariadicArgs) }
47
+
48
+ raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected at least #{signature_required.size})" unless args.size >= signature_required.size
44
49
  else
45
50
  raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size
46
51
  end
47
52
  end
48
53
 
49
54
  def check_types(args, function, signature)
55
+ # Adjust the signature for VariadicArgs to create a copy of the expected type for each given arg
56
+ if signature.last.kind_of?(VariadicArgs)
57
+ signature = signature[0..-2] + Array.new(args.size - signature.size + 1, signature.last.type)
58
+ end
59
+
50
60
  args.zip(signature).each_with_index do |(arg, type), index|
51
61
  type = type.type if type.kind_of?(OptionalArg)
52
62
 
53
- raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
63
+ if type.kind_of?(Array)
64
+ raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected one of #{type.join(", ")})" unless type.any? { |t| arg.kind_of?(t) }
65
+ else
66
+ raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
67
+ end
54
68
  end
55
69
  end
56
70
  end
@@ -63,6 +77,50 @@ module Floe
63
77
  rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
64
78
  rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
65
79
 
80
+ STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze
81
+
82
+ rule(:states_format => {:args => subtree(:args)}) do
83
+ args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Numeric, NilClass]]])
84
+ str, *rest = *args
85
+
86
+ # TODO: Handle templates with escaped characters, including invalid templates
87
+ # See https://states-language.net/#intrinsic-functions (number 6)
88
+
89
+ expected_args = str.scan(STATES_FORMAT_PLACEHOLDER).size
90
+ actual_args = rest.size
91
+ if expected_args != actual_args
92
+ raise ArgumentError, "number of arguments to States.Format do not match the occurrences of {} (given #{actual_args}, expected #{expected_args})"
93
+ end
94
+
95
+ rest.each do |arg|
96
+ str = str.sub(STATES_FORMAT_PLACEHOLDER, arg.nil? ? "null" : arg.to_s)
97
+ end
98
+
99
+ # TODO: Handle arguments that have escape characters within them but are interpolated
100
+ str.gsub!("\\'", "'")
101
+ str.gsub!("\\{", "{")
102
+ str.gsub!("\\}", "}")
103
+ str.gsub!("\\\\", "\\")
104
+
105
+ str
106
+ end
107
+
108
+ rule(:states_json_to_string => {:args => subtree(:args)}) do
109
+ args = Transformer.process_args(args(), "States.JsonToString", [Object])
110
+ json = args.first
111
+
112
+ JSON.generate(json)
113
+ end
114
+
115
+ rule(:states_string_to_json => {:args => subtree(:args)}) do
116
+ args = Transformer.process_args(args(), "States.StringToJson", [String])
117
+ str = args.first
118
+
119
+ JSON.parse(str)
120
+ rescue JSON::ParserError => e
121
+ raise ArgumentError, "invalid value for argument 1 to States.StringToJson (invalid json: #{e.message})"
122
+ end
123
+
66
124
  rule(:states_array => {:args => subtree(:args)}) do
67
125
  Transformer.process_args(args, "States.Array")
68
126
  end
@@ -133,12 +191,8 @@ module Floe
133
191
  end
134
192
 
135
193
  rule(:states_hash => {:args => subtree(:args)}) do
136
- args = Transformer.process_args(args(), "States.Hash", [Object, String])
194
+ args = Transformer.process_args(args(), "States.Hash", [[String, TrueClass, FalseClass, Numeric, Array, Hash], String])
137
195
  data, algorithm = *args
138
- raise NotImplementedError if data.kind_of?(Hash)
139
- if data.nil?
140
- raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
141
- end
142
196
 
143
197
  algorithms = %w[MD5 SHA-1 SHA-256 SHA-384 SHA-512]
144
198
  unless algorithms.include?(algorithm)
@@ -147,8 +201,20 @@ module Floe
147
201
 
148
202
  require "openssl"
149
203
  algorithm = algorithm.sub("-", "")
150
- data = data.to_json unless data.kind_of?(String)
151
- OpenSSL::Digest.hexdigest(algorithm, data)
204
+ data = JSON.generate(data) unless data.kind_of?(String)
205
+ OpenSSL::Digest.hexdigest(algorithm, data).force_encoding("UTF-8")
206
+ end
207
+
208
+ rule(:states_json_merge => {:args => subtree(:args)}) do
209
+ args = Transformer.process_args(args(), "States.JsonMerge", [Hash, Hash, [TrueClass, FalseClass]])
210
+ left, right, deep = *args
211
+
212
+ if deep
213
+ # NOTE: not implemented by AWS Step Functions and nuances not defined in docs
214
+ left.merge(right) { |_key, l, r| l.kind_of?(Hash) && r.kind_of?(Hash) ? l.merge(r) : r }
215
+ else
216
+ left.merge(right)
217
+ end
152
218
  end
153
219
 
154
220
  rule(:states_math_random => {:args => subtree(:args)}) do
@@ -34,7 +34,18 @@ module Floe
34
34
  return obj if path == "$"
35
35
 
36
36
  results = JsonPath.on(obj, path)
37
- results.count < 2 ? results.first : results
37
+ case results.count
38
+ when 0
39
+ raise Floe::PathError, "Path [#{payload}] references an invalid value"
40
+ when 1
41
+ results.first
42
+ else
43
+ results
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ payload
38
49
  end
39
50
  end
40
51
  end
@@ -48,17 +48,27 @@ module Floe
48
48
  return Errno::EAGAIN unless ready?(context)
49
49
 
50
50
  finish(context)
51
+ rescue Floe::Error => e
52
+ mark_error(context, e)
51
53
  end
52
54
 
53
55
  def start(context)
56
+ mark_started(context)
57
+ end
58
+
59
+ def finish(context)
60
+ mark_finished(context)
61
+ end
62
+
63
+ def mark_started(context)
54
64
  context.state["EnteredTime"] = Time.now.utc.iso8601
55
65
 
56
66
  logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
57
67
  end
58
68
 
59
- def finish(context)
60
- finished_time = Time.now.utc
61
- entered_time = Time.parse(context.state["EnteredTime"])
69
+ def mark_finished(context)
70
+ finished_time = Time.now.utc
71
+ entered_time = Time.parse(context.state["EnteredTime"])
62
72
 
63
73
  context.state["FinishedTime"] ||= finished_time.iso8601
64
74
  context.state["Duration"] = finished_time - entered_time
@@ -69,6 +79,14 @@ module Floe
69
79
  0
70
80
  end
71
81
 
82
+ def mark_error(context, exception)
83
+ # InputPath or OutputPath were bad.
84
+ context.next_state = nil
85
+ context.output = {"Error" => "States.Runtime", "Cause" => exception.message}
86
+ # Since finish threw an exception, super was never called. Calling that now.
87
+ mark_finished(context)
88
+ end
89
+
72
90
  def ready?(context)
73
91
  !context.state_started? || !running?(context)
74
92
  end
data/lib/floe.rb CHANGED
@@ -43,6 +43,7 @@ module Floe
43
43
  class Error < StandardError; end
44
44
  class InvalidWorkflowError < Error; end
45
45
  class InvalidExecutionInput < Error; end
46
+ class PathError < Error; end
46
47
 
47
48
  def self.logger
48
49
  @logger ||= NullLogger.new
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.12.0
4
+ version: 0.13.1
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-08-01 00:00:00.000000000 Z
11
+ date: 2024-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn