floe 0.5.0 → 0.6.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: 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.