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 +4 -4
- data/CHANGELOG.md +23 -6
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/path.rb +3 -0
- data/lib/floe/workflow/runner/docker.rb +21 -3
- data/lib/floe/workflow/runner/docker_mixin.rb +31 -0
- data/lib/floe/workflow/runner/kubernetes.rb +33 -14
- data/lib/floe/workflow/runner/podman.rb +6 -60
- data/lib/floe/workflow/state.rb +7 -3
- data/lib/floe/workflow/states/choice.rb +18 -0
- data/lib/floe/workflow/states/non_terminal_mixin.rb +14 -0
- data/lib/floe/workflow/states/pass.rb +10 -0
- data/lib/floe/workflow/states/task.rb +11 -1
- data/lib/floe/workflow/states/wait.rb +12 -7
- data/lib/floe/workflow.rb +5 -1
- data/lib/floe.rb +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58ace49051c911efbe352b2b882cc663aea3be231e6c8dcf138864e340ae2de6
|
4
|
+
data.tar.gz: d422be1ce106663dd1dc40b7ddb52024f9eb9b0e85f807572d66f31fcdf16e51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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]
|
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]
|
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]
|
43
|
-
- Move end out of the root state node ([#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]
|
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.
|
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
data/lib/floe/workflow/path.rb
CHANGED
@@ -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
|
-
|
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
|
98
|
-
|
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
|
-
|
8
|
-
|
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 =
|
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
|
-
|
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
|
-
|
76
|
-
|
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
|
95
|
-
|
110
|
+
def pod_running?(context)
|
111
|
+
RUNNING_PHASES.include?(context.dig("container_state", "phase"))
|
96
112
|
end
|
97
113
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
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
|
-
|
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 =>
|
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
|
-
|
112
|
-
params = podman_global_options + args
|
113
|
-
|
114
|
-
AwesomeSpawn.run!("podman", :params => params, **kwargs)
|
115
|
-
end
|
61
|
+
alias podman! docker!
|
116
62
|
|
117
|
-
def
|
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
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -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
|
-
|
37
|
+
wait until run_nonblock! == 0
|
34
38
|
end
|
35
39
|
|
36
|
-
def
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
45
|
-
|
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.
|
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.
|
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-
|
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.
|
157
|
+
rubygems_version: 3.4.20
|
156
158
|
signing_key:
|
157
159
|
specification_version: 4
|
158
160
|
summary: Simple Workflow Runner.
|