floe 0.4.0 → 0.4.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: e7a1c4e51ef3ec3052e8c3c150070464ed1a8b463a3b1237774b8f1b2ff416dc
4
- data.tar.gz: 8cd6fd6e8c7bed14635d906699f13dde1f5e666018f0c62709972dbddc1999cc
3
+ metadata.gz: b2bab2c07fd71a45bcf896aa219993770c83eedd5c4e3268c318d8b9ba0413ed
4
+ data.tar.gz: 786064609be7fee8a19855cb97fd160984b5a8a7aedc85344eab01302155a988
5
5
  SHA512:
6
- metadata.gz: 73bf61a41d240922786746b0ace87c616311c4bd3f63ac59883096e53b0fce766ef791db3270a29cf257cd5315f5764d69c91bcfd4817efcadfaae72be4c226b
7
- data.tar.gz: 3cb8f1ad58c7b1b7895363f46871b38b300a66eb40ebb69d2fc8ffd1b5ec303c982c0f49f89f9575b209b5e0671c2570e54670078da4e4916a3718050c9e0b9d
6
+ metadata.gz: 45a9fd8a85bb5b1059574c0dc63b0434496b6c75634c567aba9f0008400867b4dbf8c33337b3940084c644606dc3fe6065fc4f79ac5732449ca0d9675d0e2531
7
+ data.tar.gz: f4147cd4e1633d2af02b7cbbd04735a9ffdc733359eb7f349b23a2fb8bada9de18a4f37fad82de543f2eb8060fa6fa52d1864a159f3ab3f780cbdab1824b6ab1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.1] - 2023-10-06
8
+ ### Added
9
+ - Add Fail#CausePath and Fail#ErrorPath ([#110](https://github.com/ManageIQ/floe/pull/110))
10
+ - Add Task#Retrier incremental backoff and Wait#Timestamp ([#100](https://github.com/ManageIQ/floe/pull/100))
11
+
12
+ ### Fixed
13
+ - Combine stdout and stderr for docker and podman runners ([#104](https://github.com/ManageIQ/floe/pull/104))
14
+ - Don't raise an exception on task failure ([#115](https://github.com/ManageIQ/floe/pull/115))
15
+ - Fix task output handling ([#112](https://github.com/ManageIQ/floe/pull/112))
16
+ - Fix Context#input not JSON parsed ([#122](https://github.com/ManageIQ/floe/pull/122))
17
+
7
18
  ## [0.4.0] - 2023-09-26
8
19
  ### Added
9
20
  - Add ability to run workflows asynchronously ([#52](https://github.com/ManageIQ/floe/pull/92))
@@ -63,7 +74,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
63
74
  ### Added
64
75
  - Initial release
65
76
 
66
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.4.0...HEAD
77
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.4.1...HEAD
78
+ [0.4.1]: https://github.com/ManageIQ/floe/compare/v0.4.0...v0.4.1
67
79
  [0.4.0]: https://github.com/ManageIQ/floe/compare/v0.3.1...v0.4.0
68
80
  [0.3.1]: https://github.com/ManageIQ/floe/compare/v0.3.0...v0.3.1
69
81
  [0.3.0]: https://github.com/ManageIQ/floe/compare/v0.2.3...v0.3.0
data/Gemfile CHANGED
@@ -12,3 +12,4 @@ gem "manageiq-style"
12
12
  gem "rake", "~> 13.0"
13
13
  gem "rspec"
14
14
  gem "rubocop"
15
+ gem "timecop"
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.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -10,7 +10,7 @@ module Floe
10
10
 
11
11
  @error_equals = payload["ErrorEquals"]
12
12
  @next = payload["Next"]
13
- @result_path = payload.fetch("ResultPath", "$")
13
+ @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
14
14
  end
15
15
  end
16
16
  end
@@ -5,6 +5,7 @@ module Floe
5
5
  class Context
6
6
  def initialize(context = nil, input: {})
7
7
  context = JSON.parse(context) if context.kind_of?(String)
8
+ input = JSON.parse(input) if input.kind_of?(String)
8
9
 
9
10
  @context = context || {
10
11
  "Execution" => {
@@ -73,7 +73,7 @@ module Floe
73
73
  end
74
74
 
75
75
  def output(runner_context)
76
- output = docker!("logs", runner_context["container_ref"]).output
76
+ output = docker!("logs", runner_context["container_ref"], :combined_output => true).output
77
77
  runner_context["output"] = output
78
78
  end
79
79
 
@@ -83,7 +83,7 @@ module Floe
83
83
  end
84
84
 
85
85
  def output(runner_context)
86
- output = podman!("logs", runner_context["container_ref"]).output
86
+ output = podman!("logs", runner_context["container_ref"], :combined_output => true).output
87
87
  runner_context["output"] = output
88
88
  end
89
89
 
@@ -5,7 +5,8 @@ module Floe
5
5
  class Runner
6
6
  include Logging
7
7
 
8
- TYPES = %w[docker podman kubernetes].freeze
8
+ TYPES = %w[docker podman kubernetes].freeze
9
+ OUTPUT_MARKER = "__FLOE_OUTPUT__\n"
9
10
 
10
11
  def initialize(_options = {})
11
12
  end
@@ -34,23 +35,24 @@ module Floe
34
35
  raise NotImplementedError, "Must be implemented in a subclass"
35
36
  end
36
37
 
38
+ # @return [Hash] runner_context
37
39
  def run_async!(_image, _env = {}, _secrets = {})
38
40
  raise NotImplementedError, "Must be implemented in a subclass"
39
41
  end
40
42
 
41
- def running?(_ref)
43
+ def running?(_runner_context)
42
44
  raise NotImplementedError, "Must be implemented in a subclass"
43
45
  end
44
46
 
45
- def success?(_ref)
47
+ def success?(_runner_context)
46
48
  raise NotImplementedError, "Must be implemented in a subclass"
47
49
  end
48
50
 
49
- def output(_ref)
51
+ def output(_runner_context)
50
52
  raise NotImplementedError, "Must be implemented in a subclass"
51
53
  end
52
54
 
53
- def cleanup(_ref, _secret)
55
+ def cleanup(_runner_context)
54
56
  raise NotImplementedError, "Must be implemented in a subclass"
55
57
  end
56
58
  end
@@ -92,6 +92,23 @@ module Floe
92
92
  def finished?
93
93
  context.state.key?("FinishedTime")
94
94
  end
95
+
96
+ private
97
+
98
+ def wait(seconds: nil, time: nil)
99
+ context.state["WaitUntil"] =
100
+ if seconds
101
+ (Time.parse(context.state["EnteredTime"]) + seconds).iso8601
102
+ elsif time.kind_of?(String)
103
+ time
104
+ else
105
+ time.iso8601
106
+ end
107
+ end
108
+
109
+ def waiting?
110
+ context.state["WaitUntil"] && Time.now.utc <= Time.parse(context.state["WaitUntil"])
111
+ end
95
112
  end
96
113
  end
97
114
  end
@@ -9,16 +9,24 @@ module Floe
9
9
  def initialize(workflow, name, payload)
10
10
  super
11
11
 
12
- @cause = payload["Cause"]
13
- @error = payload["Error"]
12
+ @cause = payload["Cause"]
13
+ @error = payload["Error"]
14
+ @cause_path = Path.new(payload["CausePath"]) if payload["CausePath"]
15
+ @error_path = Path.new(payload["ErrorPath"]) if payload["ErrorPath"]
14
16
  end
15
17
 
16
18
  def start(input)
17
19
  super
18
- context.state["Error"] = error
19
- context.state["Cause"] = cause
20
- context.next_state = nil
21
- context.output = input
20
+ context.next_state = nil
21
+ # TODO: support intrinsic functions here
22
+ # see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html
23
+ # https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html#asl-intrsc-func-generic
24
+ context.output = {
25
+ "Error" => @error_path ? @error_path.value(context, input) : error,
26
+ "Cause" => @cause_path ? @cause_path.value(context, input) : cause
27
+ }.compact
28
+ context.state["Error"] = context.output["Error"]
29
+ context.state["Cause"] = context.output["Cause"]
22
30
  end
23
31
 
24
32
  def running?
@@ -29,10 +29,10 @@ module Floe
29
29
 
30
30
  def start(input)
31
31
  super
32
- input = input_path.value(context, input)
33
- input = parameters.value(context, input) if parameters
34
32
 
33
+ input = process_input(input)
35
34
  runner_context = runner.run_async!(resource, input, credentials&.value({}, workflow.credentials))
35
+
36
36
  context.state["RunnerContext"] = runner_context
37
37
  end
38
38
 
@@ -41,13 +41,15 @@ module Floe
41
41
  end
42
42
 
43
43
  def finish
44
- results = runner.output(context.state["RunnerContext"])
44
+ output = runner.output(context.state["RunnerContext"])
45
45
 
46
46
  if success?
47
- context.state["Output"] = process_output!(results)
47
+ output = parse_output(output)
48
+ context.state["Output"] = process_output!(output)
48
49
  context.next_state = next_state
49
50
  else
50
- retry_state!(results) || catch_error!(results)
51
+ error = parse_error(output)
52
+ retry_state!(error) || catch_error!(error) || fail_workflow!(error)
51
53
  end
52
54
 
53
55
  super
@@ -56,6 +58,8 @@ module Floe
56
58
  end
57
59
 
58
60
  def running?
61
+ return true if waiting?
62
+
59
63
  runner.status!(context.state["RunnerContext"])
60
64
  runner.running?(context.state["RunnerContext"])
61
65
  end
@@ -81,7 +85,7 @@ module Floe
81
85
  end
82
86
 
83
87
  def retry_state!(error)
84
- retrier = find_retrier(error)
88
+ retrier = find_retrier(error["Error"]) if error
85
89
  return if retrier.nil?
86
90
 
87
91
  # If a different retrier is hit reset the context
@@ -94,16 +98,24 @@ module Floe
94
98
 
95
99
  return if context["State"]["RetryCount"] > retrier.max_attempts
96
100
 
97
- # TODO: Kernel.sleep(retrier.sleep_duration(context["State"]["RetryCount"]))
101
+ wait(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
98
102
  context.next_state = context.state_name
99
103
  true
100
104
  end
101
105
 
102
106
  def catch_error!(error)
103
- catcher = find_catcher(error)
104
- raise error if catcher.nil?
107
+ catcher = find_catcher(error["Error"]) if error
108
+ return if catcher.nil?
105
109
 
106
110
  context.next_state = catcher.next
111
+ context.output = catcher.result_path.set(context.input, error)
112
+ true
113
+ end
114
+
115
+ def fail_workflow!(error)
116
+ context.next_state = nil
117
+ context.output = {"Error" => error["Error"], "Cause" => error["Cause"]}.compact
118
+ context.state["Error"] = context.output["Error"]
107
119
  end
108
120
 
109
121
  def process_input(input)
@@ -112,17 +124,27 @@ module Floe
112
124
  input
113
125
  end
114
126
 
127
+ def parse_error(output)
128
+ return if output.nil?
129
+
130
+ JSON.parse(output)
131
+ rescue JSON::ParserError
132
+ {"Error" => output}
133
+ end
134
+
135
+ def parse_output(output)
136
+ return if output.nil?
137
+
138
+ JSON.parse(output.split("\n").last)
139
+ rescue JSON::ParserError
140
+ nil
141
+ end
142
+
115
143
  def process_output!(results)
116
- output = process_input(context.state["Input"])
144
+ output = context.input.dup
117
145
  return output if results.nil?
118
146
  return if output_path.nil?
119
147
 
120
- begin
121
- results = JSON.parse(results)
122
- rescue JSON::ParserError
123
- results = {"results" => results}
124
- end
125
-
126
148
  results = result_selector.value(context, results) if result_selector
127
149
  output = result_path.set(output, results)
128
150
  output_path.value(context, output)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Floe
4
6
  class Workflow
5
7
  module States
@@ -9,9 +11,12 @@ module Floe
9
11
  def initialize(workflow, name, payload)
10
12
  super
11
13
 
12
- @next = payload["Next"]
13
- @end = !!payload["End"]
14
- @seconds = payload["Seconds"].to_i
14
+ @next = payload["Next"]
15
+ @end = !!payload["End"]
16
+ @seconds = payload["Seconds"]&.to_i
17
+ @timestamp = payload["Timestamp"]
18
+ @timestamp_path = Path.new(payload["TimestampPath"]) if payload.key?("TimestampPath")
19
+ @seconds_path = Path.new(payload["SecondsPath"]) if payload.key?("SecondsPath")
15
20
 
16
21
  @input_path = Path.new(payload.fetch("InputPath", "$"))
17
22
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
@@ -23,21 +28,25 @@ module Floe
23
28
 
24
29
  context.output = output_path.value(context, input)
25
30
  context.next_state = end? ? nil : @next
31
+ please_hold(input)
26
32
  end
27
33
 
28
34
  def running?
29
- now = Time.now.utc
30
- if now > (Time.parse(context.state["EnteredTime"]) + @seconds)
31
- context.state["FinishedTime"] = now.iso8601
32
- false
33
- else
34
- true
35
- end
35
+ waiting?
36
36
  end
37
37
 
38
38
  def end?
39
39
  @end
40
40
  end
41
+
42
+ private
43
+
44
+ def please_hold(input)
45
+ wait(
46
+ :seconds => @seconds_path ? @seconds_path.value(context, input).to_i : @seconds,
47
+ :time => @timestamp_path ? @timestamp_path.value(context, input) : @timestamp
48
+ )
49
+ end
41
50
  end
42
51
  end
43
52
  end
data/lib/floe.rb CHANGED
@@ -32,6 +32,7 @@ require_relative "floe/workflow/states/task"
32
32
  require_relative "floe/workflow/states/wait"
33
33
 
34
34
  require "jsonpath"
35
+ require "time"
35
36
 
36
37
  module Floe
37
38
  class Error < StandardError; end
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.4.0
4
+ version: 0.4.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: 2023-09-26 00:00:00.000000000 Z
11
+ date: 2023-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn