manageiq-floe 0.1.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 +7 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +11 -0
- data/README.md +46 -0
- data/Rakefile +4 -0
- data/examples/workflow.json +89 -0
- data/exe/manageiq-floe +21 -0
- data/lib/manageiq/floe/logging.rb +15 -0
- data/lib/manageiq/floe/null_logger.rb +15 -0
- data/lib/manageiq/floe/version.rb +7 -0
- data/lib/manageiq/floe/workflow/catcher.rb +19 -0
- data/lib/manageiq/floe/workflow/choice_rule/boolean.rb +21 -0
- data/lib/manageiq/floe/workflow/choice_rule/data.rb +96 -0
- data/lib/manageiq/floe/workflow/choice_rule.rb +43 -0
- data/lib/manageiq/floe/workflow/path.rb +36 -0
- data/lib/manageiq/floe/workflow/payload_template.rb +39 -0
- data/lib/manageiq/floe/workflow/reference_path.rb +46 -0
- data/lib/manageiq/floe/workflow/retrier.rb +24 -0
- data/lib/manageiq/floe/workflow/runner/docker.rb +45 -0
- data/lib/manageiq/floe/workflow/runner/kubernetes.rb +118 -0
- data/lib/manageiq/floe/workflow/runner/podman.rb +42 -0
- data/lib/manageiq/floe/workflow/runner.rb +33 -0
- data/lib/manageiq/floe/workflow/state.rb +78 -0
- data/lib/manageiq/floe/workflow/states/choice.rb +55 -0
- data/lib/manageiq/floe/workflow/states/fail.rb +39 -0
- data/lib/manageiq/floe/workflow/states/map.rb +15 -0
- data/lib/manageiq/floe/workflow/states/parallel.rb +15 -0
- data/lib/manageiq/floe/workflow/states/pass.rb +33 -0
- data/lib/manageiq/floe/workflow/states/succeed.rb +28 -0
- data/lib/manageiq/floe/workflow/states/task.rb +84 -0
- data/lib/manageiq/floe/workflow/states/wait.rb +27 -0
- data/lib/manageiq/floe/workflow.rb +84 -0
- data/lib/manageiq/floe.rb +46 -0
- data/lib/manageiq-floe.rb +3 -0
- data/manageiq-floe.gemspec +39 -0
- data/sig/manageiq/floe.rbs +6 -0
- metadata +166 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Floe
|
3
|
+
class Workflow
|
4
|
+
class Runner
|
5
|
+
class Kubernetes < ManageIQ::Floe::Workflow::Runner
|
6
|
+
attr_reader :namespace
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
require "awesome_spawn"
|
10
|
+
require "securerandom"
|
11
|
+
require "base64"
|
12
|
+
require "yaml"
|
13
|
+
|
14
|
+
@namespace = ENV.fetch("DOCKER_RUNNER_NAMESPACE", "default")
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!(resource, env = {}, secrets = {})
|
20
|
+
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
21
|
+
|
22
|
+
image = resource.sub("docker://", "")
|
23
|
+
name = pod_name(image)
|
24
|
+
secret = create_secret!(secrets) unless secrets&.empty?
|
25
|
+
overrides = pod_spec(image, env, secret)
|
26
|
+
|
27
|
+
result = kubectl_run!(image, name, overrides)
|
28
|
+
|
29
|
+
# Kubectl prints that the pod was deleted, strip this from the output
|
30
|
+
output = result.output.gsub(/pod \"#{name}\" deleted/, "")
|
31
|
+
|
32
|
+
[result.exit_status, output]
|
33
|
+
ensure
|
34
|
+
delete_secret!(secret) if secret
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def container_name(image)
|
40
|
+
image.match(%r{^(?<repository>.+\/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
|
41
|
+
end
|
42
|
+
|
43
|
+
def pod_name(image)
|
44
|
+
container_short_name = container_name(image)
|
45
|
+
raise ArgumentError, "Invalid docker image [#{image}]" if container_short_name.nil?
|
46
|
+
|
47
|
+
"#{container_short_name}-#{SecureRandom.uuid}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def pod_spec(image, env, secret = nil)
|
51
|
+
container_spec = {
|
52
|
+
"name" => container_name(image),
|
53
|
+
"image" => image,
|
54
|
+
"env" => env.to_h.map { |k, v| {"name" => k, "value" => v} }
|
55
|
+
}
|
56
|
+
|
57
|
+
spec = {"spec" => {"containers" => [container_spec]}}
|
58
|
+
|
59
|
+
if secret
|
60
|
+
spec["spec"]["volumes"] = [{"name" => "secret-volume", "secret" => {"secretName" => secret}}]
|
61
|
+
container_spec["env"] << {"name" => "SECRETS", "value" => "/run/secrets/#{secret}/secret"}
|
62
|
+
container_spec["volumeMounts"] = [
|
63
|
+
{
|
64
|
+
"name" => "secret-volume",
|
65
|
+
"mountPath" => "/run/secrets/#{secret}",
|
66
|
+
"readOnly" => true
|
67
|
+
}
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
spec
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_secret!(secrets)
|
75
|
+
secret_name = SecureRandom.uuid
|
76
|
+
|
77
|
+
secret_yaml = {
|
78
|
+
"kind" => "Secret",
|
79
|
+
"apiVersion" => "v1",
|
80
|
+
"metadata" => {
|
81
|
+
"name" => secret_name,
|
82
|
+
"namespace" => namespace
|
83
|
+
},
|
84
|
+
"data" => {
|
85
|
+
"secret" => Base64.urlsafe_encode64(secrets.to_json)
|
86
|
+
},
|
87
|
+
"type" => "Opaque"
|
88
|
+
}.to_yaml
|
89
|
+
|
90
|
+
kubectl!("create", "-f", "-", :in_data => secret_yaml)
|
91
|
+
|
92
|
+
secret_name
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete_secret!(secret_name)
|
96
|
+
kubectl!("delete", "secret", secret_name, [:namespace, namespace])
|
97
|
+
end
|
98
|
+
|
99
|
+
def kubectl!(*params, **kwargs)
|
100
|
+
AwesomeSpawn.run!("kubectl", :params => params, **kwargs)
|
101
|
+
end
|
102
|
+
|
103
|
+
def kubectl_run!(image, name, overrides = nil)
|
104
|
+
params = [
|
105
|
+
"run", :rm, :attach, [:image, image], [:restart, "Never"], [:namespace, namespace], name
|
106
|
+
]
|
107
|
+
|
108
|
+
params << "--overrides=#{overrides.to_json}" if overrides
|
109
|
+
|
110
|
+
logger.debug("Running kubectl: #{AwesomeSpawn.build_command_line("kubectl", params)}")
|
111
|
+
|
112
|
+
kubectl!(*params)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Floe
|
3
|
+
class Workflow
|
4
|
+
class Runner
|
5
|
+
class Podman < ManageIQ::Floe::Workflow::Runner
|
6
|
+
def initialize(*)
|
7
|
+
require "awesome_spawn"
|
8
|
+
require "securerandom"
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def run!(resource, env = {}, secrets = {})
|
14
|
+
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
15
|
+
|
16
|
+
image = resource.sub("docker://", "")
|
17
|
+
|
18
|
+
params = ["run", :rm]
|
19
|
+
params += env.map { |k, v| [:e, "#{k}=#{v}"] } if env
|
20
|
+
|
21
|
+
if secrets && !secrets.empty?
|
22
|
+
secret_guid = SecureRandom.uuid
|
23
|
+
AwesomeSpawn.run!("podman", :params => ["secret", "create", secret_guid, "-"], :in_data => secrets.to_json)
|
24
|
+
|
25
|
+
params << [:e, "SECRETS=/run/secrets/#{secret_guid}"]
|
26
|
+
params << [:secret, secret_guid]
|
27
|
+
end
|
28
|
+
|
29
|
+
params << image
|
30
|
+
|
31
|
+
logger.debug("Running podman: #{AwesomeSpawn.build_command_line("podman", params)}")
|
32
|
+
result = AwesomeSpawn.run!("podman", :params => params)
|
33
|
+
|
34
|
+
[result.exit_status, result.output]
|
35
|
+
ensure
|
36
|
+
AwesomeSpawn.run("podman", :params => ["secret", "rm", secret_guid]) if secret_guid
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Floe
|
3
|
+
class Workflow
|
4
|
+
class Runner
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_writer :docker_runner_klass
|
9
|
+
|
10
|
+
def docker_runner_klass
|
11
|
+
@docker_runner_klass ||= const_get(ENV.fetch("DOCKER_RUNNER", "docker").capitalize)
|
12
|
+
end
|
13
|
+
|
14
|
+
def for_resource(resource)
|
15
|
+
raise ArgumentError, "resource cannot be nil" if resource.nil?
|
16
|
+
|
17
|
+
scheme = resource.split("://").first
|
18
|
+
case scheme
|
19
|
+
when "docker"
|
20
|
+
docker_runner_klass.new
|
21
|
+
else
|
22
|
+
raise "Invalid resource scheme [#{scheme}]"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def run!(image, env = {}, secrets = {})
|
28
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
class State
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def build!(workflow, name, payload)
|
11
|
+
state_type = payload["Type"]
|
12
|
+
|
13
|
+
begin
|
14
|
+
klass = ManageIQ::Floe::Workflow::States.const_get(state_type)
|
15
|
+
rescue NameError
|
16
|
+
raise ManageIQ::Floe::InvalidWorkflowError, "Invalid state type: [#{state_type}]"
|
17
|
+
end
|
18
|
+
|
19
|
+
klass.new(workflow, name, payload)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :workflow, :comment, :name, :type, :payload
|
24
|
+
|
25
|
+
def initialize(workflow, name, payload)
|
26
|
+
@workflow = workflow
|
27
|
+
@name = name
|
28
|
+
@payload = payload
|
29
|
+
@end = !!payload["End"]
|
30
|
+
@type = payload["Type"]
|
31
|
+
@comment = payload["Comment"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def end?
|
35
|
+
@end
|
36
|
+
end
|
37
|
+
|
38
|
+
def context
|
39
|
+
workflow.context
|
40
|
+
end
|
41
|
+
|
42
|
+
def run!(input)
|
43
|
+
logger.info("Running state: [#{name}] with input [#{input}]")
|
44
|
+
|
45
|
+
input = input_path.value(context, input)
|
46
|
+
|
47
|
+
output, next_state = block_given? ? yield(input) : input
|
48
|
+
next_state ||= workflow.states_by_name[payload["Next"]] unless end?
|
49
|
+
|
50
|
+
output ||= input
|
51
|
+
output = output_path&.value(context, output)
|
52
|
+
|
53
|
+
logger.info("Running state: [#{name}] with input [#{input}]...Complete - next state: [#{next_state&.name}] output: [#{output}]")
|
54
|
+
|
55
|
+
[next_state, output]
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_dot
|
59
|
+
String.new.tap do |s|
|
60
|
+
s << " #{name}"
|
61
|
+
|
62
|
+
attributes = to_dot_attributes
|
63
|
+
s << " [ #{attributes.to_a.map { |kv| kv.join("=") }.join(" ")} ]" unless attributes.empty?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private def to_dot_attributes
|
68
|
+
end? ? {:style => "bold"} : {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_dot_transitions
|
72
|
+
next_state_name = payload["Next"] unless end?
|
73
|
+
Array(next_state_name && " #{name} -> #{next_state_name}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
module States
|
7
|
+
class Choice < ManageIQ::Floe::Workflow::State
|
8
|
+
attr_reader :choices, :default, :input_path, :output_path
|
9
|
+
|
10
|
+
def initialize(workflow, name, payload)
|
11
|
+
super
|
12
|
+
|
13
|
+
@choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
|
14
|
+
@default = payload["Default"]
|
15
|
+
|
16
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
17
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def run!(*)
|
21
|
+
super do |input|
|
22
|
+
next_state_name = choices.detect { |choice| choice.true?(context, input) }&.next || default
|
23
|
+
next_state = workflow.states_by_name[next_state_name]
|
24
|
+
|
25
|
+
output = input
|
26
|
+
|
27
|
+
[output, next_state]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private def to_dot_attributes
|
32
|
+
super.merge(:shape => "diamond")
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_dot_transitions
|
36
|
+
[].tap do |a|
|
37
|
+
choices.each do |choice|
|
38
|
+
choice_label =
|
39
|
+
if choice.payload["NumericEquals"]
|
40
|
+
"#{choice.variable} == #{choice.payload["NumericEquals"]}"
|
41
|
+
else
|
42
|
+
"Unknown" # TODO
|
43
|
+
end
|
44
|
+
|
45
|
+
a << " #{name} -> #{choice.next} [ label=#{choice_label.inspect} ]"
|
46
|
+
end
|
47
|
+
|
48
|
+
a << " #{name} -> #{default} [ label=\"Default\" ]" if default
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
module States
|
7
|
+
class Fail < ManageIQ::Floe::Workflow::State
|
8
|
+
attr_reader :cause, :error
|
9
|
+
|
10
|
+
def initialize(workflow, name, payload)
|
11
|
+
super
|
12
|
+
|
13
|
+
@cause = payload["Cause"]
|
14
|
+
@error = payload["Error"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def run!(input)
|
18
|
+
logger.info("Running state: [#{name}] with input [#{input}]")
|
19
|
+
|
20
|
+
next_state = nil
|
21
|
+
output = input
|
22
|
+
|
23
|
+
logger.info("Running state: [#{name}] with input [#{input}]...Complete - next state: [#{next_state&.name}]")
|
24
|
+
|
25
|
+
[next_state, output]
|
26
|
+
end
|
27
|
+
|
28
|
+
def end?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
private def to_dot_attributes
|
33
|
+
super.merge(:color => "red")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
module States
|
7
|
+
class Pass < ManageIQ::Floe::Workflow::State
|
8
|
+
attr_reader :end, :next, :result, :parameters, :input_path, :output_path, :result_path
|
9
|
+
|
10
|
+
def initialize(workflow, name, payload)
|
11
|
+
super
|
12
|
+
|
13
|
+
@next = payload["Next"]
|
14
|
+
@result = payload["Result"]
|
15
|
+
|
16
|
+
@parameters = PayloadTemplate.new(payload["Parameters"]) if payload["Parameters"]
|
17
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
18
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
19
|
+
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
|
20
|
+
end
|
21
|
+
|
22
|
+
def run!(*)
|
23
|
+
super do |input|
|
24
|
+
output = input
|
25
|
+
output = result_path.set(output, result) if result && result_path
|
26
|
+
output
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
module States
|
7
|
+
class Succeed < ManageIQ::Floe::Workflow::State
|
8
|
+
attr_reader :input_path, :output_path
|
9
|
+
|
10
|
+
def initialize(workflow, name, payload)
|
11
|
+
super
|
12
|
+
|
13
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
14
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
15
|
+
end
|
16
|
+
|
17
|
+
def end?
|
18
|
+
true # TODO: Handle if this is ending a parallel or map state
|
19
|
+
end
|
20
|
+
|
21
|
+
private def to_dot_attributes
|
22
|
+
super.merge(:color => "green")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
module States
|
7
|
+
class Task < ManageIQ::Floe::Workflow::State
|
8
|
+
attr_reader :credentials, :end, :heartbeat_seconds, :next, :parameters,
|
9
|
+
:result_selector, :resource, :timeout_seconds, :retry, :catch,
|
10
|
+
:input_path, :output_path, :result_path
|
11
|
+
|
12
|
+
def initialize(workflow, name, payload)
|
13
|
+
super
|
14
|
+
|
15
|
+
@heartbeat_seconds = payload["HeartbeatSeconds"]
|
16
|
+
@next = payload["Next"]
|
17
|
+
@resource = payload["Resource"]
|
18
|
+
@timeout_seconds = payload["TimeoutSeconds"]
|
19
|
+
@retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
|
20
|
+
@catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
|
21
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
22
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
23
|
+
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
|
24
|
+
@parameters = PayloadTemplate.new(payload["Parameters"]) if payload["Parameters"]
|
25
|
+
@result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
|
26
|
+
@credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def run!(*)
|
30
|
+
super do |input|
|
31
|
+
input = parameters.value(context, input) if parameters
|
32
|
+
|
33
|
+
runner = ManageIQ::Floe::Workflow::Runner.for_resource(resource)
|
34
|
+
_exit_status, results = runner.run!(resource, input, credentials&.value({}, workflow.credentials))
|
35
|
+
|
36
|
+
output = input
|
37
|
+
process_output!(output, results)
|
38
|
+
rescue => err
|
39
|
+
retrier = self.retry.detect { |r| (r.error_equals & [err.to_s, "States.ALL"]).any? }
|
40
|
+
retry if retry!(retrier)
|
41
|
+
|
42
|
+
catcher = self.catch.detect { |c| (c.error_equals & [err.to_s, "States.ALL"]).any? }
|
43
|
+
raise if catcher.nil?
|
44
|
+
|
45
|
+
[output, workflow.states_by_name[catcher.next]]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def retry!(retrier)
|
52
|
+
return if retrier.nil?
|
53
|
+
|
54
|
+
# If a different retrier is hit reset the context
|
55
|
+
if !context.key?("retrier") || context["retrier"]["error_equals"] != retrier.error_equals
|
56
|
+
context["retrier"] = {"error_equals" => retrier.error_equals, "retry_count" => 0}
|
57
|
+
end
|
58
|
+
|
59
|
+
context["retrier"]["retry_count"] += 1
|
60
|
+
|
61
|
+
return if context["retrier"]["retry_count"] > retrier.max_attempts
|
62
|
+
|
63
|
+
Kernel.sleep(retrier.sleep_duration(context["retrier"]["retry_count"]))
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_output!(output, results)
|
68
|
+
return output if results.nil?
|
69
|
+
return if output_path.nil?
|
70
|
+
|
71
|
+
begin
|
72
|
+
results = JSON.parse(results)
|
73
|
+
rescue JSON::ParserError
|
74
|
+
results = {"results" => results}
|
75
|
+
end
|
76
|
+
|
77
|
+
results = result_selector.value(context, results) if result_selector
|
78
|
+
result_path.set(output, results)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module Floe
|
5
|
+
class Workflow
|
6
|
+
module States
|
7
|
+
class Wait < ManageIQ::Floe::Workflow::State
|
8
|
+
attr_reader :end, :next, :seconds, :input_path, :output_path
|
9
|
+
|
10
|
+
def initialize(workflow, name, payload)
|
11
|
+
super
|
12
|
+
|
13
|
+
@next = payload["Next"]
|
14
|
+
@seconds = payload["Seconds"].to_i
|
15
|
+
|
16
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"), context)
|
17
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"), context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run!(*)
|
21
|
+
super { sleep(seconds); nil }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module ManageIQ
|
6
|
+
module Floe
|
7
|
+
class Workflow
|
8
|
+
class << self
|
9
|
+
def load(path_or_io, context = {}, credentials = {})
|
10
|
+
payload = path_or_io.respond_to?(:read) ? path_or_io.read : File.read(path_or_io)
|
11
|
+
new(payload, context, credentials)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :context, :credentials, :first_state, :payload, :states, :states_by_name, :start_at
|
16
|
+
|
17
|
+
def initialize(payload, context = {}, credentials = {})
|
18
|
+
payload = JSON.parse(payload) if payload.kind_of?(String)
|
19
|
+
context = JSON.parse(context) if context.kind_of?(String)
|
20
|
+
credentials = JSON.parse(credentials) if credentials.kind_of?(String)
|
21
|
+
|
22
|
+
@payload = payload
|
23
|
+
@context = context
|
24
|
+
@credentials = credentials
|
25
|
+
@states = payload["States"].to_a.map { |name, state| State.build!(self, name, state) }
|
26
|
+
@states_by_name = states.to_h { |state| [state.name, state] }
|
27
|
+
@start_at = @payload["StartAt"]
|
28
|
+
@first_state = @states_by_name[@start_at]
|
29
|
+
rescue JSON::ParserError => err
|
30
|
+
raise ManageIQ::Floe::InvalidWorkflowError, err.message
|
31
|
+
end
|
32
|
+
|
33
|
+
def run!
|
34
|
+
state = first_state
|
35
|
+
input = context.dup
|
36
|
+
|
37
|
+
until state.nil?
|
38
|
+
state, output = state.run!(input)
|
39
|
+
input = output
|
40
|
+
end
|
41
|
+
|
42
|
+
output
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_dot
|
46
|
+
String.new.tap do |s|
|
47
|
+
s << "digraph {\n"
|
48
|
+
states.each do |state|
|
49
|
+
s << state.to_dot << "\n"
|
50
|
+
end
|
51
|
+
s << "\n"
|
52
|
+
states.each do |state|
|
53
|
+
Array(state.to_dot_transitions).each do |transition|
|
54
|
+
s << transition << "\n"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
s << "}\n"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_svg(path: nil)
|
62
|
+
require "open3"
|
63
|
+
out, err, _status = Open3.capture3("dot -Tsvg", :stdin_data => to_dot)
|
64
|
+
|
65
|
+
raise "Error from graphviz:\n#{err}" if err && !err.empty?
|
66
|
+
|
67
|
+
File.write(path, out) if path
|
68
|
+
|
69
|
+
out
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_ascii(path: nil)
|
73
|
+
require "open3"
|
74
|
+
out, err, _status = Open3.capture3("graph-easy", :stdin_data => to_dot)
|
75
|
+
|
76
|
+
raise "Error from graph-easy:\n#{err}" if err && !err.empty?
|
77
|
+
|
78
|
+
File.write(path, out) if path
|
79
|
+
|
80
|
+
out
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|