floe 0.5.0 → 0.6.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: 0aac9121f1d393173b2fb11f9422f9e689693318a97dada576f054d827901024
4
- data.tar.gz: 8118d4632e54c6fa3beb94384be517d5f3bf494890855901ce36af7ea94ee1c2
3
+ metadata.gz: b92b3c488e49ea77447b370e9152c49e11036e8295c6a781feda666662927f9e
4
+ data.tar.gz: c553deab6491c9876e28a816cb2422a1318ac6f91a9f9f75cbfc5a144a84d248
5
5
  SHA512:
6
- metadata.gz: 3872e70c2bb41e46570bcba95ef63716cdf3ea93bed77e9654db6614f8225982e13e3a83256b97854b6aa4376799e803f2edd5066dc3e304e75e2970c55d60f2
7
- data.tar.gz: fac7e8815bcff8d93e443653759a6cdf8371c78f9114e3fef133212c0a1ef49d76bb35a0f491ef5abfeb134a21dc11cbbc00a6694d1f05d0e90f5bdae3471c41
6
+ metadata.gz: 3f5c210d2a745aaed8e3419e228465203dcebb02ee678c1d2b47285083e1f154118c25864e51360d3d062ca0cfade9baa84f5a932804b586f26607a195acc710
7
+ data.tar.gz: 238fd469915e8682c449abd26e1950f68fd4b032a32fcace11130109d46dc6962b29e4ce381734d167f5328ef16ee2fdd4bb250f844644abebc571fc001ab2dd
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.1] - 2023-11-21
8
+ ### Fixed
9
+ - Return an error payload if run_async! fails ([#143](https://github.com/ManageIQ/floe/pull/143))
10
+
11
+ ### Changed
12
+ - Extract run_container_params for docker/podman ([#142](https://github.com/ManageIQ/floe/pull/142))
13
+
14
+ ## [0.6.0] - 2023-11-09
15
+ ### Added
16
+ - Prefix pod names with 'floe-' ([#132](https://github.com/ManageIQ/floe/pull/132))
17
+ - Validate that the workflow payload is correct ([#136](https://github.com/ManageIQ/floe/pull/136))
18
+
19
+ ### Fixed
20
+ - Fix issue where certain docker image names cannot be pod names ([#134](https://github.com/ManageIQ/floe/pull/134))
21
+ - Fix uninitialized constant RSpec::Support::Differ in tests ([#137](https://github.com/ManageIQ/floe/pull/137))
22
+ - Handle ImagePullErr/ImagePullBackOff as errors ([#135](https://github.com/ManageIQ/floe/pull/135))
23
+
24
+ ### Changed
25
+ - Add task spec helper ([#123](https://github.com/ManageIQ/floe/pull/123))
26
+ - Rename State#run_wait to just #wait ([#139](https://github.com/ManageIQ/floe/pull/139))
27
+ - Refactor the Podman runner to be a Docker subclass ([#140](https://github.com/ManageIQ/floe/pull/140))
28
+
7
29
  ## [0.5.0] - 2023-10-12
8
30
  ### Added
9
31
  - For task errors, use the json on the last line ([#128](https://github.com/ManageIQ/floe/pull/128))
@@ -32,19 +54,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
32
54
 
33
55
  ## [0.3.1] - 2023-08-29
34
56
  ### Added
35
- - Add more global podman runner options ([#90])(https://github.com/ManageIQ/floe/pull/90)
57
+ - Add more global podman runner options ([#90](https://github.com/ManageIQ/floe/pull/90))
36
58
 
37
59
  ## [0.3.0] - 2023-08-07
38
60
  ### Added
39
- - Add --network=host option to Docker/Podman runners ([#81])(https://github.com/ManageIQ/floe/pull/81)
61
+ - Add --network=host option to Docker/Podman runners ([#81](https://github.com/ManageIQ/floe/pull/81))
40
62
 
41
63
  ### Fixed
42
- - Fix PayloadTemplate value transformation rules ([#78])(https://github.com/ManageIQ/floe/pull/78)
43
- - Move end out of the root state node ([#80])(https://github.com/ManageIQ/floe/pull/80)
64
+ - Fix PayloadTemplate value transformation rules ([#78](https://github.com/ManageIQ/floe/pull/78))
65
+ - Move end out of the root state node ([#80](https://github.com/ManageIQ/floe/pull/80))
44
66
 
45
67
  ## [0.2.3] - 2023-07-28
46
68
  ### Fixed
47
- - Fix storing next_state in Context ([#76])(https://github.com/ManageIQ/floe/pull/76)
69
+ - Fix storing next_state in Context ([#76](https://github.com/ManageIQ/floe/pull/76))
48
70
 
49
71
  ## [0.2.2] - 2023-07-24
50
72
  ### Fixed
@@ -84,7 +106,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
84
106
  ### Added
85
107
  - Initial release
86
108
 
87
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.4.1...HEAD
109
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.6.1...HEAD
110
+ [0.6.1]: https://github.com/ManageIQ/floe/compare/v0.6.0...v0.6.1
111
+ [0.6.0]: https://github.com/ManageIQ/floe/compare/v0.5.0...v0.6.0
112
+ [0.5.0]: https://github.com/ManageIQ/floe/compare/v0.4.1...v0.5.0
88
113
  [0.4.1]: https://github.com/ManageIQ/floe/compare/v0.4.0...v0.4.1
89
114
  [0.4.0]: https://github.com/ManageIQ/floe/compare/v0.3.1...v0.4.0
90
115
  [0.3.1]: https://github.com/ManageIQ/floe/compare/v0.3.0...v0.3.1
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.5.0".freeze
4
+ VERSION = "0.6.1".freeze
5
5
  end
@@ -11,6 +11,9 @@ module Floe
11
11
 
12
12
  def initialize(payload)
13
13
  @payload = payload
14
+
15
+ raise Floe::InvalidWorkflowError, "Path [#{payload}] must be a string" if payload.nil? || !payload.kind_of?(String)
16
+ raise Floe::InvalidWorkflowError, "Path [#{payload}] must start with \"$\"" if payload[0] != "$"
14
17
  end
15
18
 
16
19
  def value(context, input = {})
@@ -4,6 +4,10 @@ module Floe
4
4
  class Workflow
5
5
  class Runner
6
6
  class Docker < Floe::Workflow::Runner
7
+ include DockerMixin
8
+
9
+ DOCKER_COMMAND = "docker"
10
+
7
11
  def initialize(options = {})
8
12
  require "awesome_spawn"
9
13
  require "tempfile"
@@ -26,27 +30,28 @@ module Floe
26
30
 
27
31
  begin
28
32
  runner_context["container_ref"] = run_container(image, env, runner_context["secrets_ref"])
29
- rescue
33
+ runner_context
34
+ rescue AwesomeSpawn::CommandResultError => err
30
35
  cleanup(runner_context)
31
- raise
36
+ {"Error" => "States.TaskFailed", "Cause" => err.to_s}
32
37
  end
33
-
34
- runner_context
35
38
  end
36
39
 
37
40
  def cleanup(runner_context)
38
41
  container_id, secrets_file = runner_context.values_at("container_ref", "secrets_ref")
39
42
 
40
43
  delete_container(container_id) if container_id
41
- File.unlink(secrets_file) if secrets_file && File.exist?(secrets_file)
44
+ delete_secret(secrets_file) if secrets_file
42
45
  end
43
46
 
44
47
  def status!(runner_context)
48
+ return if runner_context.key?("Error")
49
+
45
50
  runner_context["container_state"] = inspect_container(runner_context["container_ref"]).first&.dig("State")
46
51
  end
47
52
 
48
53
  def running?(runner_context)
49
- runner_context.dig("container_state", "Running")
54
+ !!runner_context.dig("container_state", "Running")
50
55
  end
51
56
 
52
57
  def success?(runner_context)
@@ -54,6 +59,8 @@ module Floe
54
59
  end
55
60
 
56
61
  def output(runner_context)
62
+ return runner_context.slice("Error", "Cause") if runner_context.key?("Error")
63
+
57
64
  output = docker!("logs", runner_context["container_ref"], :combined_output => true).output
58
65
  runner_context["output"] = output
59
66
  end
@@ -63,18 +70,23 @@ module Floe
63
70
  attr_reader :network
64
71
 
65
72
  def run_container(image, env, secrets_file)
73
+ params = run_container_params(image, env, secrets_file)
74
+
75
+ logger.debug("Running #{AwesomeSpawn.build_command_line("docker", params)}")
76
+
77
+ result = docker!(*params)
78
+ result.output
79
+ end
80
+
81
+ def run_container_params(image, env, secrets_file)
66
82
  params = ["run"]
67
83
  params << :detach
68
84
  params += env.map { |k, v| [:e, "#{k}=#{v}"] }
69
85
  params << [:e, "_CREDENTIALS=/run/secrets"] if secrets_file
70
86
  params << [:net, "host"] if @network == "host"
71
87
  params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
88
+ params << [:name, container_name(image)]
72
89
  params << image
73
-
74
- logger.debug("Running docker: #{AwesomeSpawn.build_command_line("docker", params)}")
75
-
76
- result = docker!(*params)
77
- result.output
78
90
  end
79
91
 
80
92
  def inspect_container(container_id)
@@ -87,6 +99,14 @@ module Floe
87
99
  nil
88
100
  end
89
101
 
102
+ def delete_secret(secrets_file)
103
+ return unless File.exist?(secrets_file)
104
+
105
+ File.unlink(secrets_file)
106
+ rescue
107
+ nil
108
+ end
109
+
90
110
  def create_secret(secrets)
91
111
  secrets_file = Tempfile.new
92
112
  secrets_file.write(secrets.to_json)
@@ -94,8 +114,13 @@ module Floe
94
114
  secrets_file.path
95
115
  end
96
116
 
97
- def docker!(*params, **kwargs)
98
- AwesomeSpawn.run!("docker", :params => params, **kwargs)
117
+ def global_docker_options
118
+ []
119
+ end
120
+
121
+ def docker!(*args, **kwargs)
122
+ params = global_docker_options + args
123
+ AwesomeSpawn.run!(self.class::DOCKER_COMMAND, :params => params, **kwargs)
99
124
  end
100
125
  end
101
126
  end
@@ -0,0 +1,31 @@
1
+ module Floe
2
+ class Workflow
3
+ class Runner
4
+ module DockerMixin
5
+ def image_name(image)
6
+ image.match(%r{^(?<repository>.+/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
7
+ end
8
+
9
+ MAX_CONTAINER_NAME_SIZE = 63 - 5 - 9 # 63 is the max kubernetes pod name length
10
+ # -5 for the "floe-" prefix
11
+ # -9 for the random hex suffix and leading hyphen
12
+
13
+ def container_name(image)
14
+ name = image_name(image)
15
+ raise ArgumentError, "Invalid docker image [#{image}]" if name.nil?
16
+
17
+ # Normalize the image name to be used in the container name.
18
+ # This follows RFC 1123 Label names in Kubernetes as they are the most restrictive
19
+ # See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
20
+ # and https://github.com/kubernetes/kubernetes/blob/952a9cb0/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L178-L184
21
+ #
22
+ # This does not follow the leading and trailing character restriction because we will embed it
23
+ # below with a prefix and suffix that already conform to the RFC.
24
+ normalized_name = name.downcase.gsub(/[^a-z0-9-]/, "-")[0, MAX_CONTAINER_NAME_SIZE]
25
+
26
+ "floe-#{normalized_name}-#{SecureRandom.hex(4)}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,10 +4,15 @@ module Floe
4
4
  class Workflow
5
5
  class Runner
6
6
  class Kubernetes < Floe::Workflow::Runner
7
- TOKEN_FILE = "/run/secrets/kubernetes.io/serviceaccount/token"
8
- CA_CERT_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
7
+ include DockerMixin
8
+
9
+ TOKEN_FILE = "/run/secrets/kubernetes.io/serviceaccount/token"
10
+ CA_CERT_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
11
+ RUNNING_PHASES = %w[Pending Running].freeze
12
+ FAILURE_REASONS = %w[CrashLoopBackOff ImagePullBackOff ErrImagePull].freeze
9
13
 
10
14
  def initialize(options = {})
15
+ require "active_support/core_ext/hash/keys"
11
16
  require "awesome_spawn"
12
17
  require "securerandom"
13
18
  require "base64"
@@ -44,27 +49,34 @@ module Floe
44
49
  raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
45
50
 
46
51
  image = resource.sub("docker://", "")
47
- name = pod_name(image)
52
+ name = container_name(image)
48
53
  secret = create_secret!(secrets) if secrets && !secrets.empty?
49
54
 
50
55
  runner_context = {"container_ref" => name, "secrets_ref" => secret}
51
56
 
52
57
  begin
53
58
  create_pod!(name, image, env, secret)
54
- rescue
59
+ runner_context
60
+ rescue Kubeclient::HttpError => err
55
61
  cleanup(runner_context)
56
- raise
62
+ {"Error" => "States.TaskFailed", "Cause" => err.to_s}
57
63
  end
58
-
59
- runner_context
60
64
  end
61
65
 
62
66
  def status!(runner_context)
63
- runner_context["container_state"] = pod_info(runner_context["container_ref"])["status"]
67
+ return if runner_context.key?("Error")
68
+
69
+ runner_context["container_state"] = pod_info(runner_context["container_ref"]).to_h.deep_stringify_keys["status"]
64
70
  end
65
71
 
66
72
  def running?(runner_context)
67
- %w[Pending Running].include?(runner_context.dig("container_state", "phase"))
73
+ return false unless pod_running?(runner_context)
74
+ # If a pod is Pending and the containers are waiting with a failure
75
+ # reason such as ImagePullBackOff or CrashLoopBackOff then the pod
76
+ # will never be run.
77
+ return false if container_failed?(runner_context)
78
+
79
+ true
68
80
  end
69
81
 
70
82
  def success?(runner_context)
@@ -72,8 +84,14 @@ module Floe
72
84
  end
73
85
 
74
86
  def output(runner_context)
75
- output = kubeclient.get_pod_log(runner_context["container_ref"], namespace).body
76
- runner_context["output"] = output
87
+ if runner_context.key?("Error")
88
+ runner_context.slice("Error", "Cause")
89
+ elsif container_failed?(runner_context)
90
+ failed_state = failed_container_states(runner_context).first
91
+ {"Error" => failed_state["reason"], "Cause" => failed_state["message"]}
92
+ else
93
+ runner_context["output"] = kubeclient.get_pod_log(runner_context["container_ref"], namespace).body
94
+ end
77
95
  end
78
96
 
79
97
  def cleanup(runner_context)
@@ -91,15 +109,18 @@ module Floe
91
109
  kubeclient.get_pod(pod_name, namespace)
92
110
  end
93
111
 
94
- def container_name(image)
95
- image.match(%r{^(?<repository>.+/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
112
+ def pod_running?(context)
113
+ RUNNING_PHASES.include?(context.dig("container_state", "phase"))
96
114
  end
97
115
 
98
- def pod_name(image)
99
- container_short_name = container_name(image)
100
- raise ArgumentError, "Invalid docker image [#{image}]" if container_short_name.nil?
116
+ def failed_container_states(context)
117
+ container_statuses = context.dig("container_state", "containerStatuses") || []
118
+ container_statuses.map { |status| status["state"]&.values&.first }.compact
119
+ .select { |state| FAILURE_REASONS.include?(state["reason"]) }
120
+ end
101
121
 
102
- "#{container_short_name}-#{SecureRandom.uuid}"
122
+ def container_failed?(context)
123
+ failed_container_states(context).any?
103
124
  end
104
125
 
105
126
  def pod_spec(name, image, env, secret = nil)
@@ -113,7 +134,7 @@ module Floe
113
134
  :spec => {
114
135
  :containers => [
115
136
  {
116
- :name => container_name(image),
137
+ :name => name[0...-9], # remove the random suffix and its leading hyphen
117
138
  :image => image,
118
139
  :env => env.map { |k, v| {:name => k, :value => v.to_s} }
119
140
  }
@@ -3,7 +3,9 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class Runner
6
- class Podman < Floe::Workflow::Runner
6
+ class Podman < Floe::Workflow::Runner::Docker
7
+ DOCKER_COMMAND = "podman"
8
+
7
9
  def initialize(options = {})
8
10
  require "awesome_spawn"
9
11
  require "securerandom"
@@ -26,74 +28,17 @@ module Floe
26
28
  @volumepath = options["volumepath"]
27
29
  end
28
30
 
29
- def run_async!(resource, env = {}, secrets = {})
30
- raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
31
-
32
- image = resource.sub("docker://", "")
33
-
34
- if secrets && !secrets.empty?
35
- secret_guid = create_secret(secrets)
36
- end
37
-
38
- begin
39
- container_id = run_container(image, env, secret_guid)
40
- rescue
41
- cleanup({"container_ref" => container_id, "secrets_ref" => secret_guid})
42
- raise
43
- end
44
-
45
- {"container_ref" => container_id, "secrets_ref" => secret_guid}
46
- end
47
-
48
- def cleanup(runner_context)
49
- container_id, secret_guid = runner_context.values_at("container_ref", "secrets_ref")
50
-
51
- delete_container(container_id) if container_id
52
- delete_secret(secret_guid) if secret_guid
53
- end
54
-
55
- def status!(runner_context)
56
- runner_context["container_state"] = inspect_container(runner_context["container_ref"]).first&.dig("State")
57
- end
58
-
59
- def running?(runner_context)
60
- runner_context.dig("container_state", "Running")
61
- end
62
-
63
- def success?(runner_context)
64
- runner_context.dig("container_state", "ExitCode") == 0
65
- end
66
-
67
- def output(runner_context)
68
- output = podman!("logs", runner_context["container_ref"], :combined_output => true).output
69
- runner_context["output"] = output
70
- end
71
-
72
31
  private
73
32
 
74
- def run_container(image, env, secret)
33
+ def run_container_params(image, env, secret)
75
34
  params = ["run"]
76
35
  params << :detach
77
36
  params += env.map { |k, v| [:e, "#{k}=#{v}"] }
78
37
  params << [:e, "_CREDENTIALS=/run/secrets/#{secret}"] if secret
79
38
  params << [:net, "host"] if @network == "host"
80
39
  params << [:secret, secret] if secret
40
+ params << [:name, container_name(image)]
81
41
  params << image
82
-
83
- logger.debug("Running podman: #{AwesomeSpawn.build_command_line("podman", params)}")
84
-
85
- result = podman!(*params)
86
- result.output
87
- end
88
-
89
- def inspect_container(container_id)
90
- JSON.parse(podman!("inspect", container_id).output)
91
- end
92
-
93
- def delete_container(container_id)
94
- podman!("rm", container_id)
95
- rescue
96
- nil
97
42
  end
98
43
 
99
44
  def create_secret(secrets)
@@ -108,13 +53,9 @@ module Floe
108
53
  nil
109
54
  end
110
55
 
111
- def podman!(*args, **kwargs)
112
- params = podman_global_options + args
113
-
114
- AwesomeSpawn.run!("podman", :params => params, **kwargs)
115
- end
56
+ alias podman! docker!
116
57
 
117
- def podman_global_options
58
+ def global_docker_options
118
59
  options = []
119
60
  options << [:identity, @identity] if @identity
120
61
  options << [:"log-level", @log_level] if @log_level
@@ -8,6 +8,7 @@ module Floe
8
8
  class << self
9
9
  def build!(workflow, name, payload)
10
10
  state_type = payload["Type"]
11
+ raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
11
12
 
12
13
  begin
13
14
  klass = Floe::Workflow::States.const_get(state_type)
@@ -27,13 +28,16 @@ module Floe
27
28
  @payload = payload
28
29
  @type = payload["Type"]
29
30
  @comment = payload["Comment"]
31
+
32
+ raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
33
+ raise Floe::InvalidWorkflowError, "State name [#{name}] must be less than or equal to 80 characters" if name.length > 80
30
34
  end
31
35
 
32
36
  def run!(_input = nil)
33
- run_wait until run_nonblock! == 0
37
+ wait until run_nonblock! == 0
34
38
  end
35
39
 
36
- def run_wait(timeout: 5)
40
+ def wait(timeout: 5)
37
41
  start = Time.now.utc
38
42
 
39
43
  loop do
@@ -95,7 +99,7 @@ module Floe
95
99
 
96
100
  private
97
101
 
98
- def wait(seconds: nil, time: nil)
102
+ def wait_until!(seconds: nil, time: nil)
99
103
  context.state["WaitUntil"] =
100
104
  if seconds
101
105
  (Time.parse(context.state["EnteredTime"]) + seconds).iso8601
@@ -9,6 +9,8 @@ module Floe
9
9
  def initialize(workflow, name, payload)
10
10
  super
11
11
 
12
+ validate_state!
13
+
12
14
  @choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
13
15
  @default = payload["Default"]
14
16
 
@@ -33,6 +35,22 @@ module Floe
33
35
  def end?
34
36
  false
35
37
  end
38
+
39
+ private
40
+
41
+ def validate_state!
42
+ validate_state_choices!
43
+ validate_state_default!
44
+ end
45
+
46
+ def validate_state_choices!
47
+ raise Floe::InvalidWorkflowError, "Choice state must have \"Choices\"" unless payload.key?("Choices")
48
+ raise Floe::InvalidWorkflowError, "\"Choices\" must be a non-empty array" unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
49
+ end
50
+
51
+ def validate_state_default!
52
+ raise Floe::InvalidWorkflowError, "\"Default\" not in \"States\"" unless workflow.payload["States"].include?(payload["Default"])
53
+ end
36
54
  end
37
55
  end
38
56
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ module States
6
+ module NonTerminalMixin
7
+ def validate_state_next!
8
+ raise Floe::InvalidWorkflowError, "Missing \"Next\" field in state [#{name}]" if @next.nil? && !@end
9
+ raise Floe::InvalidWorkflowError, "\"Next\" [#{@next}] not in \"States\" for state [#{name}]" if @next && !workflow.payload["States"].key?(@next)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -4,6 +4,8 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  class Pass < Floe::Workflow::State
7
+ include NonTerminalMixin
8
+
7
9
  attr_reader :end, :next, :result, :parameters, :input_path, :output_path, :result_path
8
10
 
9
11
  def initialize(workflow, name, payload)
@@ -17,6 +19,8 @@ module Floe
17
19
  @input_path = Path.new(payload.fetch("InputPath", "$"))
18
20
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
19
21
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
22
+
23
+ validate_state!
20
24
  end
21
25
 
22
26
  def start(input)
@@ -36,6 +40,12 @@ module Floe
36
40
  def end?
37
41
  @end
38
42
  end
43
+
44
+ private
45
+
46
+ def validate_state!
47
+ validate_state_next!
48
+ end
39
49
  end
40
50
  end
41
51
  end
@@ -4,6 +4,8 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  class Task < Floe::Workflow::State
7
+ include NonTerminalMixin
8
+
7
9
  attr_reader :credentials, :end, :heartbeat_seconds, :next, :parameters,
8
10
  :result_selector, :resource, :timeout_seconds, :retry, :catch,
9
11
  :input_path, :output_path, :result_path
@@ -25,6 +27,8 @@ module Floe
25
27
  @parameters = PayloadTemplate.new(payload["Parameters"]) if payload["Parameters"]
26
28
  @result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
27
29
  @credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
30
+
31
+ validate_state!
28
32
  end
29
33
 
30
34
  def start(input)
@@ -72,6 +76,10 @@ module Floe
72
76
 
73
77
  attr_reader :runner
74
78
 
79
+ def validate_state!
80
+ validate_state_next!
81
+ end
82
+
75
83
  def success?
76
84
  runner.success?(context.state["RunnerContext"])
77
85
  end
@@ -98,7 +106,7 @@ module Floe
98
106
 
99
107
  return if context["State"]["RetryCount"] > retrier.max_attempts
100
108
 
101
- wait(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
109
+ wait_until!(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
102
110
  context.next_state = context.state_name
103
111
  true
104
112
  end
@@ -126,6 +134,7 @@ module Floe
126
134
 
127
135
  def parse_error(output)
128
136
  return if output.nil?
137
+ return output if output.kind_of?(Hash)
129
138
 
130
139
  JSON.parse(output.split("\n").last)
131
140
  rescue JSON::ParserError
@@ -134,6 +143,7 @@ module Floe
134
143
 
135
144
  def parse_output(output)
136
145
  return if output.nil?
146
+ return output if output.kind_of?(Hash)
137
147
 
138
148
  JSON.parse(output.split("\n").last)
139
149
  rescue JSON::ParserError
@@ -6,7 +6,9 @@ module Floe
6
6
  class Workflow
7
7
  module States
8
8
  class Wait < Floe::Workflow::State
9
- attr_reader :end, :next, :seconds, :input_path, :output_path
9
+ include NonTerminalMixin
10
+
11
+ attr_reader :end, :input_path, :next, :seconds, :seconds_path, :timestamp, :timestamp_path, :output_path
10
12
 
11
13
  def initialize(workflow, name, payload)
12
14
  super
@@ -20,6 +22,8 @@ module Floe
20
22
 
21
23
  @input_path = Path.new(payload.fetch("InputPath", "$"))
22
24
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
25
+
26
+ validate_state!
23
27
  end
24
28
 
25
29
  def start(input)
@@ -28,7 +32,11 @@ module Floe
28
32
 
29
33
  context.output = output_path.value(context, input)
30
34
  context.next_state = end? ? nil : @next
31
- please_hold(input)
35
+
36
+ wait_until!(
37
+ :seconds => seconds_path ? seconds_path.value(context, input).to_i : seconds,
38
+ :time => timestamp_path ? timestamp_path.value(context, input) : timestamp
39
+ )
32
40
  end
33
41
 
34
42
  def running?
@@ -41,11 +49,8 @@ module Floe
41
49
 
42
50
  private
43
51
 
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
- )
52
+ def validate_state!
53
+ validate_state_next!
49
54
  end
50
55
  end
51
56
  end
data/lib/floe/workflow.rb CHANGED
@@ -38,6 +38,10 @@ module Floe
38
38
  credentials = JSON.parse(credentials) if credentials.kind_of?(String)
39
39
  context = Context.new(context) unless context.kind_of?(Context)
40
40
 
41
+ raise Floe::InvalidWorkflowError, "Missing field \"States\"" if payload["States"].nil?
42
+ raise Floe::InvalidWorkflowError, "Missing field \"StartAt\"" if payload["StartAt"].nil?
43
+ raise Floe::InvalidWorkflowError, "\"StartAt\" not in the \"States\" field" unless payload["States"].key?(payload["StartAt"])
44
+
41
45
  @payload = payload
42
46
  @context = context
43
47
  @credentials = credentials
@@ -77,7 +81,7 @@ module Floe
77
81
  end
78
82
 
79
83
  def step_nonblock_wait(timeout: 5)
80
- current_state.run_wait(:timeout => timeout)
84
+ current_state.wait(:timeout => timeout)
81
85
  end
82
86
 
83
87
  def step_nonblock_ready?
data/lib/floe.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "floe/workflow/payload_template"
18
18
  require_relative "floe/workflow/reference_path"
19
19
  require_relative "floe/workflow/retrier"
20
20
  require_relative "floe/workflow/runner"
21
+ require_relative "floe/workflow/runner/docker_mixin"
21
22
  require_relative "floe/workflow/runner/docker"
22
23
  require_relative "floe/workflow/runner/kubernetes"
23
24
  require_relative "floe/workflow/runner/podman"
@@ -25,6 +26,7 @@ require_relative "floe/workflow/state"
25
26
  require_relative "floe/workflow/states/choice"
26
27
  require_relative "floe/workflow/states/fail"
27
28
  require_relative "floe/workflow/states/map"
29
+ require_relative "floe/workflow/states/non_terminal_mixin"
28
30
  require_relative "floe/workflow/states/parallel"
29
31
  require_relative "floe/workflow/states/pass"
30
32
  require_relative "floe/workflow/states/succeed"
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.5.0
4
+ version: 0.6.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-10-12 00:00:00.000000000 Z
11
+ date: 2023-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn
@@ -117,12 +117,14 @@ files:
117
117
  - lib/floe/workflow/retrier.rb
118
118
  - lib/floe/workflow/runner.rb
119
119
  - lib/floe/workflow/runner/docker.rb
120
+ - lib/floe/workflow/runner/docker_mixin.rb
120
121
  - lib/floe/workflow/runner/kubernetes.rb
121
122
  - lib/floe/workflow/runner/podman.rb
122
123
  - lib/floe/workflow/state.rb
123
124
  - lib/floe/workflow/states/choice.rb
124
125
  - lib/floe/workflow/states/fail.rb
125
126
  - lib/floe/workflow/states/map.rb
127
+ - lib/floe/workflow/states/non_terminal_mixin.rb
126
128
  - lib/floe/workflow/states/parallel.rb
127
129
  - lib/floe/workflow/states/pass.rb
128
130
  - lib/floe/workflow/states/succeed.rb
@@ -152,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
154
  - !ruby/object:Gem::Version
153
155
  version: '0'
154
156
  requirements: []
155
- rubygems_version: 3.2.33
157
+ rubygems_version: 3.4.20
156
158
  signing_key:
157
159
  specification_version: 4
158
160
  summary: Simple Workflow Runner.