floe 0.4.0 → 0.4.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: 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