floe 0.12.0 → 0.13.1

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