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