floe 0.5.0 → 0.6.0

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: 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.