floe 0.5.0 → 0.6.0

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: 58ace49051c911efbe352b2b882cc663aea3be231e6c8dcf138864e340ae2de6
4
+ data.tar.gz: d422be1ce106663dd1dc40b7ddb52024f9eb9b0e85f807572d66f31fcdf16e51
5
5
  SHA512:
6
- metadata.gz: 3872e70c2bb41e46570bcba95ef63716cdf3ea93bed77e9654db6614f8225982e13e3a83256b97854b6aa4376799e803f2edd5066dc3e304e75e2970c55d60f2
7
- data.tar.gz: fac7e8815bcff8d93e443653759a6cdf8371c78f9114e3fef133212c0a1ef49d76bb35a0f491ef5abfeb134a21dc11cbbc00a6694d1f05d0e90f5bdae3471c41
6
+ metadata.gz: e5eab9d1763c723d8bb913e31bb736d0ba3c3e6d6d63585031e30adc5cbd3049105c756b89eee6999df2d6a0dcfd35e6a1ae4a39c10ce949387e91aae30acf32
7
+ data.tar.gz: 3249cce513b7195ce5ed791c300d9df62159febeb522c45db36dda38244c76f9290af3157ac744fa0757417ce327f95eea80c704375310ed3181e05426208e3b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.0] - 2023-11-09
8
+ ### Added
9
+ - Prefix pod names with 'floe-' ([#132](https://github.com/ManageIQ/floe/pull/132))
10
+ - Validate that the workflow payload is correct ([#136](https://github.com/ManageIQ/floe/pull/136))
11
+
12
+ ### Fixed
13
+ - Fix issue where certain docker image names cannot be pod names ([#134](https://github.com/ManageIQ/floe/pull/134))
14
+ - Fix uninitialized constant RSpec::Support::Differ in tests ([#137](https://github.com/ManageIQ/floe/pull/137))
15
+ - Handle ImagePullErr/ImagePullBackOff as errors ([#135](https://github.com/ManageIQ/floe/pull/135))
16
+
17
+ ### Changed
18
+ - Add task spec helper ([#123](https://github.com/ManageIQ/floe/pull/123))
19
+ - Rename State#run_wait to just #wait ([#139](https://github.com/ManageIQ/floe/pull/139))
20
+ - Refactor the Podman runner to be a Docker subclass ([#140](https://github.com/ManageIQ/floe/pull/140))
21
+
7
22
  ## [0.5.0] - 2023-10-12
8
23
  ### Added
9
24
  - For task errors, use the json on the last line ([#128](https://github.com/ManageIQ/floe/pull/128))
@@ -32,19 +47,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
32
47
 
33
48
  ## [0.3.1] - 2023-08-29
34
49
  ### Added
35
- - Add more global podman runner options ([#90])(https://github.com/ManageIQ/floe/pull/90)
50
+ - Add more global podman runner options ([#90](https://github.com/ManageIQ/floe/pull/90))
36
51
 
37
52
  ## [0.3.0] - 2023-08-07
38
53
  ### Added
39
- - Add --network=host option to Docker/Podman runners ([#81])(https://github.com/ManageIQ/floe/pull/81)
54
+ - Add --network=host option to Docker/Podman runners ([#81](https://github.com/ManageIQ/floe/pull/81))
40
55
 
41
56
  ### 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)
57
+ - Fix PayloadTemplate value transformation rules ([#78](https://github.com/ManageIQ/floe/pull/78))
58
+ - Move end out of the root state node ([#80](https://github.com/ManageIQ/floe/pull/80))
44
59
 
45
60
  ## [0.2.3] - 2023-07-28
46
61
  ### Fixed
47
- - Fix storing next_state in Context ([#76])(https://github.com/ManageIQ/floe/pull/76)
62
+ - Fix storing next_state in Context ([#76](https://github.com/ManageIQ/floe/pull/76))
48
63
 
49
64
  ## [0.2.2] - 2023-07-24
50
65
  ### Fixed
@@ -84,7 +99,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
84
99
  ### Added
85
100
  - Initial release
86
101
 
87
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.4.1...HEAD
102
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.6.0...HEAD
103
+ [0.6.0]: https://github.com/ManageIQ/floe/compare/v0.5.0...v0.6.0
104
+ [0.5.0]: https://github.com/ManageIQ/floe/compare/v0.4.1...v0.5.0
88
105
  [0.4.1]: https://github.com/ManageIQ/floe/compare/v0.4.0...v0.4.1
89
106
  [0.4.0]: https://github.com/ManageIQ/floe/compare/v0.3.1...v0.4.0
90
107
  [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.0".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"
@@ -38,7 +42,7 @@ module Floe
38
42
  container_id, secrets_file = runner_context.values_at("container_ref", "secrets_ref")
39
43
 
40
44
  delete_container(container_id) if container_id
41
- File.unlink(secrets_file) if secrets_file && File.exist?(secrets_file)
45
+ delete_secret(secrets_file) if secrets_file
42
46
  end
43
47
 
44
48
  def status!(runner_context)
@@ -69,6 +73,7 @@ module Floe
69
73
  params << [:e, "_CREDENTIALS=/run/secrets"] if secrets_file
70
74
  params << [:net, "host"] if @network == "host"
71
75
  params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
76
+ params << [:name, container_name(image)]
72
77
  params << image
73
78
 
74
79
  logger.debug("Running docker: #{AwesomeSpawn.build_command_line("docker", params)}")
@@ -87,6 +92,14 @@ module Floe
87
92
  nil
88
93
  end
89
94
 
95
+ def delete_secret(secrets_file)
96
+ return unless File.exist?(secrets_file)
97
+
98
+ File.unlink(secrets_file)
99
+ rescue
100
+ nil
101
+ end
102
+
90
103
  def create_secret(secrets)
91
104
  secrets_file = Tempfile.new
92
105
  secrets_file.write(secrets.to_json)
@@ -94,8 +107,13 @@ module Floe
94
107
  secrets_file.path
95
108
  end
96
109
 
97
- def docker!(*params, **kwargs)
98
- AwesomeSpawn.run!("docker", :params => params, **kwargs)
110
+ def global_docker_options
111
+ []
112
+ end
113
+
114
+ def docker!(*args, **kwargs)
115
+ params = global_docker_options + args
116
+ AwesomeSpawn.run!(self.class::DOCKER_COMMAND, :params => params, **kwargs)
99
117
  end
100
118
  end
101
119
  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,7 +49,7 @@ 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}
@@ -60,11 +65,17 @@ module Floe
60
65
  end
61
66
 
62
67
  def status!(runner_context)
63
- runner_context["container_state"] = pod_info(runner_context["container_ref"])["status"]
68
+ runner_context["container_state"] = pod_info(runner_context["container_ref"]).to_h.deep_stringify_keys["status"]
64
69
  end
65
70
 
66
71
  def running?(runner_context)
67
- %w[Pending Running].include?(runner_context.dig("container_state", "phase"))
72
+ return false unless pod_running?(runner_context)
73
+ # If a pod is Pending and the containers are waiting with a failure
74
+ # reason such as ImagePullBackOff or CrashLoopBackOff then the pod
75
+ # will never be run.
76
+ return false if container_failed?(runner_context)
77
+
78
+ true
68
79
  end
69
80
 
70
81
  def success?(runner_context)
@@ -72,8 +83,13 @@ module Floe
72
83
  end
73
84
 
74
85
  def output(runner_context)
75
- output = kubeclient.get_pod_log(runner_context["container_ref"], namespace).body
76
- runner_context["output"] = output
86
+ runner_context["output"] =
87
+ if container_failed?(runner_context)
88
+ failed_state = failed_container_states(runner_context).first
89
+ {"Error" => failed_state["reason"], "Cause" => failed_state["message"]}
90
+ else
91
+ kubeclient.get_pod_log(runner_context["container_ref"], namespace).body
92
+ end
77
93
  end
78
94
 
79
95
  def cleanup(runner_context)
@@ -91,15 +107,18 @@ module Floe
91
107
  kubeclient.get_pod(pod_name, namespace)
92
108
  end
93
109
 
94
- def container_name(image)
95
- image.match(%r{^(?<repository>.+/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
110
+ def pod_running?(context)
111
+ RUNNING_PHASES.include?(context.dig("container_state", "phase"))
96
112
  end
97
113
 
98
- def pod_name(image)
99
- container_short_name = container_name(image)
100
- raise ArgumentError, "Invalid docker image [#{image}]" if container_short_name.nil?
114
+ def failed_container_states(context)
115
+ container_statuses = context.dig("container_state", "containerStatuses") || []
116
+ container_statuses.map { |status| status["state"]&.values&.first }.compact
117
+ .select { |state| FAILURE_REASONS.include?(state["reason"]) }
118
+ end
101
119
 
102
- "#{container_short_name}-#{SecureRandom.uuid}"
120
+ def container_failed?(context)
121
+ failed_container_states(context).any?
103
122
  end
104
123
 
105
124
  def pod_spec(name, image, env, secret = nil)
@@ -113,7 +132,7 @@ module Floe
113
132
  :spec => {
114
133
  :containers => [
115
134
  {
116
- :name => container_name(image),
135
+ :name => name[0...-9], # remove the random suffix and its leading hyphen
117
136
  :image => image,
118
137
  :env => env.map { |k, v| {:name => k, :value => v.to_s} }
119
138
  }
@@ -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,49 +28,6 @@ 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
33
  def run_container(image, env, secret)
@@ -78,6 +37,7 @@ module Floe
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
42
 
83
43
  logger.debug("Running podman: #{AwesomeSpawn.build_command_line("podman", params)}")
@@ -86,16 +46,6 @@ module Floe
86
46
  result.output
87
47
  end
88
48
 
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
- end
98
-
99
49
  def create_secret(secrets)
100
50
  secret_guid = SecureRandom.uuid
101
51
  podman!("secret", "create", secret_guid, "-", :in_data => secrets.to_json)
@@ -108,13 +58,9 @@ module Floe
108
58
  nil
109
59
  end
110
60
 
111
- def podman!(*args, **kwargs)
112
- params = podman_global_options + args
113
-
114
- AwesomeSpawn.run!("podman", :params => params, **kwargs)
115
- end
61
+ alias podman! docker!
116
62
 
117
- def podman_global_options
63
+ def global_docker_options
118
64
  options = []
119
65
  options << [:identity, @identity] if @identity
120
66
  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.0
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-09 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.