floe 0.4.1 → 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 +33 -6
- data/examples/workflow.asl +4 -4
- data/exe/floe +1 -0
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/path.rb +3 -0
- data/lib/floe/workflow/runner/docker.rb +25 -25
- data/lib/floe/workflow/runner/docker_mixin.rb +31 -0
- data/lib/floe/workflow/runner/kubernetes.rb +37 -42
- data/lib/floe/workflow/runner/podman.rb +9 -80
- 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 +13 -3
- 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,31 @@ 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
|
+
|
|
22
|
+
## [0.5.0] - 2023-10-12
|
|
23
|
+
### Added
|
|
24
|
+
- For task errors, use the json on the last line ([#128](https://github.com/ManageIQ/floe/pull/128))
|
|
25
|
+
- Add ability to pass task service account to kube runner ([#131](https://github.com/ManageIQ/floe/pull/131))
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Don't put credentials file into input ([#124](https://github.com/ManageIQ/floe/pull/124))
|
|
29
|
+
- exe/floe return success status if the workflow was successful ([#129](https://github.com/ManageIQ/floe/pull/129))
|
|
30
|
+
- For error output, drop trailing newline ([#126](https://github.com/ManageIQ/floe/pull/126))
|
|
31
|
+
|
|
7
32
|
## [0.4.1] - 2023-10-06
|
|
8
33
|
### Added
|
|
9
34
|
- Add Fail#CausePath and Fail#ErrorPath ([#110](https://github.com/ManageIQ/floe/pull/110))
|
|
@@ -22,19 +47,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
22
47
|
|
|
23
48
|
## [0.3.1] - 2023-08-29
|
|
24
49
|
### Added
|
|
25
|
-
- Add more global podman runner options ([#90]
|
|
50
|
+
- Add more global podman runner options ([#90](https://github.com/ManageIQ/floe/pull/90))
|
|
26
51
|
|
|
27
52
|
## [0.3.0] - 2023-08-07
|
|
28
53
|
### Added
|
|
29
|
-
- 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))
|
|
30
55
|
|
|
31
56
|
### Fixed
|
|
32
|
-
- Fix PayloadTemplate value transformation rules ([#78]
|
|
33
|
-
- 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))
|
|
34
59
|
|
|
35
60
|
## [0.2.3] - 2023-07-28
|
|
36
61
|
### Fixed
|
|
37
|
-
- Fix storing next_state in Context ([#76]
|
|
62
|
+
- Fix storing next_state in Context ([#76](https://github.com/ManageIQ/floe/pull/76))
|
|
38
63
|
|
|
39
64
|
## [0.2.2] - 2023-07-24
|
|
40
65
|
### Fixed
|
|
@@ -74,7 +99,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
74
99
|
### Added
|
|
75
100
|
- Initial release
|
|
76
101
|
|
|
77
|
-
[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
|
|
78
105
|
[0.4.1]: https://github.com/ManageIQ/floe/compare/v0.4.0...v0.4.1
|
|
79
106
|
[0.4.0]: https://github.com/ManageIQ/floe/compare/v0.3.1...v0.4.0
|
|
80
107
|
[0.3.1]: https://github.com/ManageIQ/floe/compare/v0.3.0...v0.3.1
|
data/examples/workflow.asl
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"States": {
|
|
5
5
|
"FirstState": {
|
|
6
6
|
"Type": "Task",
|
|
7
|
-
"Resource": "docker://agrare/hello-world:latest",
|
|
7
|
+
"Resource": "docker://docker.io/agrare/hello-world:latest",
|
|
8
8
|
"Credentials": {
|
|
9
9
|
"mysecret": "dont tell anyone"
|
|
10
10
|
},
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
|
|
50
50
|
"FirstMatchState": {
|
|
51
51
|
"Type" : "Task",
|
|
52
|
-
"Resource": "docker://agrare/hello-world:latest",
|
|
52
|
+
"Resource": "docker://docker.io/agrare/hello-world:latest",
|
|
53
53
|
"Next": "PassState"
|
|
54
54
|
},
|
|
55
55
|
|
|
56
56
|
"SecondMatchState": {
|
|
57
57
|
"Type" : "Task",
|
|
58
|
-
"Resource": "docker://agrare/hello-world:latest",
|
|
58
|
+
"Resource": "docker://docker.io/agrare/hello-world:latest",
|
|
59
59
|
"Next": "NextState"
|
|
60
60
|
},
|
|
61
61
|
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
|
|
88
88
|
"NextState": {
|
|
89
89
|
"Type": "Task",
|
|
90
|
-
"Resource": "docker://agrare/hello-world:latest",
|
|
90
|
+
"Resource": "docker://docker.io/agrare/hello-world:latest",
|
|
91
91
|
"Secrets": ["vmdb:aaa-bbb-ccc"],
|
|
92
92
|
"End": true
|
|
93
93
|
}
|
data/exe/floe
CHANGED
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"
|
|
@@ -13,24 +17,6 @@ module Floe
|
|
|
13
17
|
@network = options.fetch("network", "bridge")
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
def run!(resource, env = {}, secrets = {})
|
|
17
|
-
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
18
|
-
|
|
19
|
-
image = resource.sub("docker://", "")
|
|
20
|
-
|
|
21
|
-
secrets_file = nil
|
|
22
|
-
if secrets && !secrets.empty?
|
|
23
|
-
secrets_file = create_secret(secrets)
|
|
24
|
-
env["_CREDENTIALS"] = "/run/secrets"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
output = run_container(image, env, secrets_file)
|
|
28
|
-
|
|
29
|
-
{"exit_code" => 0, "output" => output}
|
|
30
|
-
ensure
|
|
31
|
-
cleanup({"secrets_ref" => secrets_file})
|
|
32
|
-
end
|
|
33
|
-
|
|
34
20
|
def run_async!(resource, env = {}, secrets = {})
|
|
35
21
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
36
22
|
|
|
@@ -40,11 +26,10 @@ module Floe
|
|
|
40
26
|
|
|
41
27
|
if secrets && !secrets.empty?
|
|
42
28
|
runner_context["secrets_ref"] = create_secret(secrets)
|
|
43
|
-
env["_CREDENTIALS"] = "/run/secrets"
|
|
44
29
|
end
|
|
45
30
|
|
|
46
31
|
begin
|
|
47
|
-
runner_context["container_ref"] = run_container(image, env, runner_context["secrets_ref"]
|
|
32
|
+
runner_context["container_ref"] = run_container(image, env, runner_context["secrets_ref"])
|
|
48
33
|
rescue
|
|
49
34
|
cleanup(runner_context)
|
|
50
35
|
raise
|
|
@@ -57,7 +42,7 @@ module Floe
|
|
|
57
42
|
container_id, secrets_file = runner_context.values_at("container_ref", "secrets_ref")
|
|
58
43
|
|
|
59
44
|
delete_container(container_id) if container_id
|
|
60
|
-
|
|
45
|
+
delete_secret(secrets_file) if secrets_file
|
|
61
46
|
end
|
|
62
47
|
|
|
63
48
|
def status!(runner_context)
|
|
@@ -81,12 +66,14 @@ module Floe
|
|
|
81
66
|
|
|
82
67
|
attr_reader :network
|
|
83
68
|
|
|
84
|
-
def run_container(image, env, secrets_file
|
|
69
|
+
def run_container(image, env, secrets_file)
|
|
85
70
|
params = ["run"]
|
|
86
|
-
params <<
|
|
71
|
+
params << :detach
|
|
87
72
|
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
|
73
|
+
params << [:e, "_CREDENTIALS=/run/secrets"] if secrets_file
|
|
88
74
|
params << [:net, "host"] if @network == "host"
|
|
89
75
|
params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
|
|
76
|
+
params << [:name, container_name(image)]
|
|
90
77
|
params << image
|
|
91
78
|
|
|
92
79
|
logger.debug("Running docker: #{AwesomeSpawn.build_command_line("docker", params)}")
|
|
@@ -105,6 +92,14 @@ module Floe
|
|
|
105
92
|
nil
|
|
106
93
|
end
|
|
107
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
|
+
|
|
108
103
|
def create_secret(secrets)
|
|
109
104
|
secrets_file = Tempfile.new
|
|
110
105
|
secrets_file.write(secrets.to_json)
|
|
@@ -112,8 +107,13 @@ module Floe
|
|
|
112
107
|
secrets_file.path
|
|
113
108
|
end
|
|
114
109
|
|
|
115
|
-
def
|
|
116
|
-
|
|
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)
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
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"
|
|
@@ -35,42 +40,16 @@ module Floe
|
|
|
35
40
|
|
|
36
41
|
@namespace = options.fetch("namespace", "default")
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def run!(resource, env = {}, secrets = {})
|
|
42
|
-
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
43
|
-
|
|
44
|
-
image = resource.sub("docker://", "")
|
|
45
|
-
name = pod_name(image)
|
|
46
|
-
secret = create_secret!(secrets) if secrets && !secrets.empty?
|
|
47
|
-
|
|
48
|
-
begin
|
|
49
|
-
runner_context = {"container_ref" => name}
|
|
43
|
+
@task_service_account = options["task_service_account"]
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
loop do
|
|
53
|
-
case pod_info(name).dig("status", "phase")
|
|
54
|
-
when "Pending", "Running"
|
|
55
|
-
sleep(1)
|
|
56
|
-
else # also "Succeeded"
|
|
57
|
-
runner_context["exit_code"] = 0
|
|
58
|
-
output(runner_context)
|
|
59
|
-
break
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
runner_context
|
|
64
|
-
ensure
|
|
65
|
-
cleanup({"container_ref" => name, "secrets_ref" => secret})
|
|
66
|
-
end
|
|
45
|
+
super
|
|
67
46
|
end
|
|
68
47
|
|
|
69
48
|
def run_async!(resource, env = {}, secrets = {})
|
|
70
49
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
71
50
|
|
|
72
51
|
image = resource.sub("docker://", "")
|
|
73
|
-
name =
|
|
52
|
+
name = container_name(image)
|
|
74
53
|
secret = create_secret!(secrets) if secrets && !secrets.empty?
|
|
75
54
|
|
|
76
55
|
runner_context = {"container_ref" => name, "secrets_ref" => secret}
|
|
@@ -86,11 +65,17 @@ module Floe
|
|
|
86
65
|
end
|
|
87
66
|
|
|
88
67
|
def status!(runner_context)
|
|
89
|
-
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"]
|
|
90
69
|
end
|
|
91
70
|
|
|
92
71
|
def running?(runner_context)
|
|
93
|
-
|
|
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
|
|
94
79
|
end
|
|
95
80
|
|
|
96
81
|
def success?(runner_context)
|
|
@@ -98,8 +83,13 @@ module Floe
|
|
|
98
83
|
end
|
|
99
84
|
|
|
100
85
|
def output(runner_context)
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
103
93
|
end
|
|
104
94
|
|
|
105
95
|
def cleanup(runner_context)
|
|
@@ -117,15 +107,18 @@ module Floe
|
|
|
117
107
|
kubeclient.get_pod(pod_name, namespace)
|
|
118
108
|
end
|
|
119
109
|
|
|
120
|
-
def
|
|
121
|
-
|
|
110
|
+
def pod_running?(context)
|
|
111
|
+
RUNNING_PHASES.include?(context.dig("container_state", "phase"))
|
|
122
112
|
end
|
|
123
113
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
119
|
|
|
128
|
-
|
|
120
|
+
def container_failed?(context)
|
|
121
|
+
failed_container_states(context).any?
|
|
129
122
|
end
|
|
130
123
|
|
|
131
124
|
def pod_spec(name, image, env, secret = nil)
|
|
@@ -139,7 +132,7 @@ module Floe
|
|
|
139
132
|
:spec => {
|
|
140
133
|
:containers => [
|
|
141
134
|
{
|
|
142
|
-
:name =>
|
|
135
|
+
:name => name[0...-9], # remove the random suffix and its leading hyphen
|
|
143
136
|
:image => image,
|
|
144
137
|
:env => env.map { |k, v| {:name => k, :value => v.to_s} }
|
|
145
138
|
}
|
|
@@ -148,6 +141,8 @@ module Floe
|
|
|
148
141
|
}
|
|
149
142
|
}
|
|
150
143
|
|
|
144
|
+
spec[:spec][:serviceAccountName] = @task_service_account if @task_service_account
|
|
145
|
+
|
|
151
146
|
if secret
|
|
152
147
|
spec[:spec][:volumes] = [
|
|
153
148
|
{
|
|
@@ -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,75 +28,16 @@ module Floe
|
|
|
26
28
|
@volumepath = options["volumepath"]
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def run!(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 = create_secret(secrets)
|
|
36
|
-
env["_CREDENTIALS"] = "/run/secrets/#{secret}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
output = run_container(image, env, secret)
|
|
40
|
-
|
|
41
|
-
{"exit_code" => 0, :output => output}
|
|
42
|
-
ensure
|
|
43
|
-
delete_secret(secret) if secret
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def run_async!(resource, env = {}, secrets = {})
|
|
47
|
-
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
48
|
-
|
|
49
|
-
image = resource.sub("docker://", "")
|
|
50
|
-
|
|
51
|
-
if secrets && !secrets.empty?
|
|
52
|
-
secret_guid = create_secret(secrets)
|
|
53
|
-
env["_CREDENTIALS"] = "/run/secrets/#{secret_guid}"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
begin
|
|
57
|
-
container_id = run_container(image, env, secret_guid, :detached => true)
|
|
58
|
-
rescue
|
|
59
|
-
cleanup({"container_ref" => container_id, "secrets_ref" => secret_guid})
|
|
60
|
-
raise
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
{"container_ref" => container_id, "secrets_ref" => secret_guid}
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def cleanup(runner_context)
|
|
67
|
-
container_id, secret_guid = runner_context.values_at("container_ref", "secrets_ref")
|
|
68
|
-
|
|
69
|
-
delete_container(container_id) if container_id
|
|
70
|
-
delete_secret(secret_guid) if secret_guid
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def status!(runner_context)
|
|
74
|
-
runner_context["container_state"] = inspect_container(runner_context["container_ref"]).first&.dig("State")
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def running?(runner_context)
|
|
78
|
-
runner_context.dig("container_state", "Running")
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def success?(runner_context)
|
|
82
|
-
runner_context.dig("container_state", "ExitCode") == 0
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def output(runner_context)
|
|
86
|
-
output = podman!("logs", runner_context["container_ref"], :combined_output => true).output
|
|
87
|
-
runner_context["output"] = output
|
|
88
|
-
end
|
|
89
|
-
|
|
90
31
|
private
|
|
91
32
|
|
|
92
|
-
def run_container(image, env, secret
|
|
33
|
+
def run_container(image, env, secret)
|
|
93
34
|
params = ["run"]
|
|
94
|
-
params <<
|
|
35
|
+
params << :detach
|
|
95
36
|
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
|
37
|
+
params << [:e, "_CREDENTIALS=/run/secrets/#{secret}"] if secret
|
|
96
38
|
params << [:net, "host"] if @network == "host"
|
|
97
39
|
params << [:secret, secret] if secret
|
|
40
|
+
params << [:name, container_name(image)]
|
|
98
41
|
params << image
|
|
99
42
|
|
|
100
43
|
logger.debug("Running podman: #{AwesomeSpawn.build_command_line("podman", params)}")
|
|
@@ -103,16 +46,6 @@ module Floe
|
|
|
103
46
|
result.output
|
|
104
47
|
end
|
|
105
48
|
|
|
106
|
-
def inspect_container(container_id)
|
|
107
|
-
JSON.parse(podman!("inspect", container_id).output)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def delete_container(container_id)
|
|
111
|
-
podman!("rm", container_id)
|
|
112
|
-
rescue
|
|
113
|
-
nil
|
|
114
|
-
end
|
|
115
|
-
|
|
116
49
|
def create_secret(secrets)
|
|
117
50
|
secret_guid = SecureRandom.uuid
|
|
118
51
|
podman!("secret", "create", secret_guid, "-", :in_data => secrets.to_json)
|
|
@@ -125,13 +58,9 @@ module Floe
|
|
|
125
58
|
nil
|
|
126
59
|
end
|
|
127
60
|
|
|
128
|
-
|
|
129
|
-
params = podman_global_options + args
|
|
130
|
-
|
|
131
|
-
AwesomeSpawn.run!("podman", :params => params, **kwargs)
|
|
132
|
-
end
|
|
61
|
+
alias podman! docker!
|
|
133
62
|
|
|
134
|
-
def
|
|
63
|
+
def global_docker_options
|
|
135
64
|
options = []
|
|
136
65
|
options << [:identity, @identity] if @identity
|
|
137
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,14 +134,16 @@ 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
|
-
JSON.parse(output)
|
|
139
|
+
JSON.parse(output.split("\n").last)
|
|
131
140
|
rescue JSON::ParserError
|
|
132
|
-
{"Error" => output}
|
|
141
|
+
{"Error" => output.chomp}
|
|
133
142
|
end
|
|
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.
|