floe 0.10.0 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/exe/floe +35 -28
- data/lib/floe/container_runner/docker.rb +225 -0
- data/lib/floe/container_runner/docker_mixin.rb +32 -0
- data/lib/floe/container_runner/kubernetes.rb +329 -0
- data/lib/floe/container_runner/podman.rb +104 -0
- data/lib/floe/container_runner.rb +61 -0
- data/lib/floe/runner.rb +82 -0
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/context.rb +3 -1
- data/lib/floe/workflow/states/non_terminal_mixin.rb +3 -1
- data/lib/floe/workflow/states/pass.rb +2 -3
- data/lib/floe/workflow/states/task.rb +2 -7
- data/lib/floe.rb +2 -18
- metadata +8 -7
- data/lib/floe/workflow/runner/docker.rb +0 -227
- data/lib/floe/workflow/runner/docker_mixin.rb +0 -32
- data/lib/floe/workflow/runner/kubernetes.rb +0 -331
- data/lib/floe/workflow/runner/podman.rb +0 -106
- data/lib/floe/workflow/runner.rb +0 -77
@@ -0,0 +1,329 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class ContainerRunner
|
5
|
+
class Kubernetes < Floe::Runner
|
6
|
+
include Floe::ContainerRunner::DockerMixin
|
7
|
+
|
8
|
+
TOKEN_FILE = "/run/secrets/kubernetes.io/serviceaccount/token"
|
9
|
+
CA_CERT_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
10
|
+
RUNNING_PHASES = %w[Pending Running].freeze
|
11
|
+
FAILURE_REASONS = %w[CrashLoopBackOff ImagePullBackOff ErrImagePull].freeze
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
require "active_support/core_ext/hash/keys"
|
15
|
+
require "awesome_spawn"
|
16
|
+
require "securerandom"
|
17
|
+
require "base64"
|
18
|
+
require "kubeclient"
|
19
|
+
require "yaml"
|
20
|
+
|
21
|
+
@kubeconfig_file = ENV.fetch("KUBECONFIG", nil) || options.fetch("kubeconfig", File.join(Dir.home, ".kube", "config"))
|
22
|
+
@kubeconfig_context = options["kubeconfig_context"]
|
23
|
+
|
24
|
+
@token = options["token"]
|
25
|
+
@token ||= File.read(options["token_file"]) if options.key?("token_file")
|
26
|
+
@token ||= File.read(TOKEN_FILE) if File.exist?(TOKEN_FILE)
|
27
|
+
|
28
|
+
@server = options["server"]
|
29
|
+
@server ||= URI::HTTPS.build(:host => ENV.fetch("KUBERNETES_SERVICE_HOST"), :port => ENV.fetch("KUBERNETES_SERVICE_PORT", 6443)) if ENV.key?("KUBERNETES_SERVICE_HOST")
|
30
|
+
|
31
|
+
@ca_file = options["ca_file"]
|
32
|
+
@ca_file ||= CA_CERT_FILE if File.exist?(CA_CERT_FILE)
|
33
|
+
|
34
|
+
@verify_ssl = options["verify_ssl"] == "false" ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
35
|
+
|
36
|
+
if server.nil? && token.nil? && !File.exist?(kubeconfig_file)
|
37
|
+
raise ArgumentError, "Missing connections options, provide a kubeconfig file or pass server and token via --docker-runner-options"
|
38
|
+
end
|
39
|
+
|
40
|
+
@namespace = options.fetch("namespace", "default")
|
41
|
+
|
42
|
+
@pull_policy = options["pull-policy"]
|
43
|
+
@task_service_account = options["task_service_account"]
|
44
|
+
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_async!(resource, env = {}, secrets = {}, _context = {})
|
49
|
+
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
50
|
+
|
51
|
+
image = resource.sub("docker://", "")
|
52
|
+
name = container_name(image)
|
53
|
+
secret = create_secret!(secrets) if secrets && !secrets.empty?
|
54
|
+
|
55
|
+
runner_context = {"container_ref" => name, "container_state" => {"phase" => "Pending"}, "secrets_ref" => secret}
|
56
|
+
|
57
|
+
begin
|
58
|
+
create_pod!(name, image, env, secret)
|
59
|
+
runner_context
|
60
|
+
rescue Kubeclient::HttpError => err
|
61
|
+
cleanup(runner_context)
|
62
|
+
{"Error" => "States.TaskFailed", "Cause" => err.to_s}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def status!(runner_context)
|
67
|
+
return if runner_context.key?("Error")
|
68
|
+
|
69
|
+
runner_context["container_state"] = pod_info(runner_context["container_ref"]).to_h.deep_stringify_keys["status"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def running?(runner_context)
|
73
|
+
return false unless pod_running?(runner_context)
|
74
|
+
# If a pod is Pending and the containers are waiting with a failure
|
75
|
+
# reason such as ImagePullBackOff or CrashLoopBackOff then the pod
|
76
|
+
# will never be run.
|
77
|
+
return false if container_failed?(runner_context)
|
78
|
+
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def success?(runner_context)
|
83
|
+
runner_context.dig("container_state", "phase") == "Succeeded"
|
84
|
+
end
|
85
|
+
|
86
|
+
def output(runner_context)
|
87
|
+
if runner_context.key?("Error")
|
88
|
+
runner_context.slice("Error", "Cause")
|
89
|
+
elsif container_failed?(runner_context)
|
90
|
+
failed_state = failed_container_states(runner_context).first
|
91
|
+
{"Error" => failed_state["reason"], "Cause" => failed_state["message"]}
|
92
|
+
else
|
93
|
+
runner_context["output"] = kubeclient.get_pod_log(runner_context["container_ref"], namespace).body
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def cleanup(runner_context)
|
98
|
+
pod, secret = runner_context.values_at("container_ref", "secrets_ref")
|
99
|
+
|
100
|
+
delete_pod(pod) if pod
|
101
|
+
delete_secret(secret) if secret
|
102
|
+
end
|
103
|
+
|
104
|
+
def wait(timeout: nil, events: %i[create update delete])
|
105
|
+
retry_connection = true
|
106
|
+
|
107
|
+
begin
|
108
|
+
watcher = kubeclient.watch_pods(:namespace => namespace)
|
109
|
+
|
110
|
+
retry_connection = true
|
111
|
+
|
112
|
+
if timeout.to_i > 0
|
113
|
+
timeout_thread = Thread.new do
|
114
|
+
sleep(timeout)
|
115
|
+
watcher.finish
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
watcher.each do |notice|
|
120
|
+
break if error_notice?(notice)
|
121
|
+
|
122
|
+
event = kube_notice_type_to_event(notice.type)
|
123
|
+
next unless events.include?(event)
|
124
|
+
|
125
|
+
runner_context = parse_notice(notice)
|
126
|
+
next if runner_context.nil?
|
127
|
+
|
128
|
+
if block_given?
|
129
|
+
yield [event, runner_context]
|
130
|
+
else
|
131
|
+
timeout_thread&.kill # If we break out before the timeout, kill the timeout thread
|
132
|
+
return [[event, runner_context]]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
rescue Kubeclient::HttpError => err
|
136
|
+
raise unless err.error_code == 401 && retry_connection
|
137
|
+
|
138
|
+
@kubeclient = nil
|
139
|
+
retry_connection = false
|
140
|
+
retry
|
141
|
+
ensure
|
142
|
+
begin
|
143
|
+
watch&.finish
|
144
|
+
rescue
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
timeout_thread&.join(0)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
attr_reader :ca_file, :kubeconfig_file, :kubeconfig_context, :namespace, :server, :token, :verify_ssl
|
155
|
+
|
156
|
+
def pod_info(pod_name)
|
157
|
+
kubeclient.get_pod(pod_name, namespace)
|
158
|
+
end
|
159
|
+
|
160
|
+
def pod_running?(context)
|
161
|
+
RUNNING_PHASES.include?(context.dig("container_state", "phase"))
|
162
|
+
end
|
163
|
+
|
164
|
+
def failed_container_states(context)
|
165
|
+
container_statuses = context.dig("container_state", "containerStatuses") || []
|
166
|
+
container_statuses.filter_map { |status| status["state"]&.values&.first }
|
167
|
+
.select { |state| FAILURE_REASONS.include?(state["reason"]) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def container_failed?(context)
|
171
|
+
failed_container_states(context).any?
|
172
|
+
end
|
173
|
+
|
174
|
+
def pod_spec(name, image, env, secret = nil)
|
175
|
+
spec = {
|
176
|
+
:kind => "Pod",
|
177
|
+
:apiVersion => "v1",
|
178
|
+
:metadata => {
|
179
|
+
:name => name,
|
180
|
+
:namespace => namespace
|
181
|
+
},
|
182
|
+
:spec => {
|
183
|
+
:containers => [
|
184
|
+
{
|
185
|
+
:name => name[0...-9], # remove the random suffix and its leading hyphen
|
186
|
+
:image => image,
|
187
|
+
:env => env.map { |k, v| {:name => k, :value => v.to_s} }
|
188
|
+
}
|
189
|
+
],
|
190
|
+
:restartPolicy => "Never"
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
spec[:spec][:imagePullPolicy] = @pull_policy if @pull_policy
|
195
|
+
spec[:spec][:serviceAccountName] = @task_service_account if @task_service_account
|
196
|
+
|
197
|
+
if secret
|
198
|
+
spec[:spec][:volumes] = [
|
199
|
+
{
|
200
|
+
:name => "secret-volume",
|
201
|
+
:secret => {:secretName => secret}
|
202
|
+
}
|
203
|
+
]
|
204
|
+
|
205
|
+
spec[:spec][:containers][0][:env] << {
|
206
|
+
:name => "_CREDENTIALS",
|
207
|
+
:value => "/run/secrets/#{secret}/secret"
|
208
|
+
}
|
209
|
+
|
210
|
+
spec[:spec][:containers][0][:volumeMounts] = [
|
211
|
+
{
|
212
|
+
:name => "secret-volume",
|
213
|
+
:mountPath => "/run/secrets/#{secret}",
|
214
|
+
:readOnly => true
|
215
|
+
}
|
216
|
+
]
|
217
|
+
end
|
218
|
+
|
219
|
+
spec
|
220
|
+
end
|
221
|
+
|
222
|
+
def create_pod!(name, image, env, secret = nil)
|
223
|
+
kubeclient.create_pod(pod_spec(name, image, env, secret))
|
224
|
+
end
|
225
|
+
|
226
|
+
def delete_pod!(name)
|
227
|
+
kubeclient.delete_pod(name, namespace)
|
228
|
+
end
|
229
|
+
|
230
|
+
def delete_pod(name)
|
231
|
+
delete_pod!(name)
|
232
|
+
rescue
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
|
236
|
+
def create_secret!(secrets)
|
237
|
+
secret_name = SecureRandom.uuid
|
238
|
+
|
239
|
+
secret_config = {
|
240
|
+
:kind => "Secret",
|
241
|
+
:apiVersion => "v1",
|
242
|
+
:metadata => {
|
243
|
+
:name => secret_name,
|
244
|
+
:namespace => namespace
|
245
|
+
},
|
246
|
+
:data => {
|
247
|
+
:secret => Base64.urlsafe_encode64(secrets.to_json)
|
248
|
+
},
|
249
|
+
:type => "Opaque"
|
250
|
+
}
|
251
|
+
|
252
|
+
kubeclient.create_secret(secret_config)
|
253
|
+
|
254
|
+
secret_name
|
255
|
+
end
|
256
|
+
|
257
|
+
def delete_secret!(secret_name)
|
258
|
+
kubeclient.delete_secret(secret_name, namespace)
|
259
|
+
end
|
260
|
+
|
261
|
+
def delete_secret(name)
|
262
|
+
delete_secret!(name)
|
263
|
+
rescue
|
264
|
+
nil
|
265
|
+
end
|
266
|
+
|
267
|
+
def kube_notice_type_to_event(type)
|
268
|
+
case type
|
269
|
+
when "ADDED"
|
270
|
+
:create
|
271
|
+
when "MODIFIED"
|
272
|
+
:update
|
273
|
+
when "DELETED"
|
274
|
+
:delete
|
275
|
+
else
|
276
|
+
:unknown
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def error_notice?(notice)
|
281
|
+
return false unless notice.type == "ERROR"
|
282
|
+
|
283
|
+
message = notice.object&.message
|
284
|
+
code = notice.object&.code
|
285
|
+
reason = notice.object&.reason
|
286
|
+
|
287
|
+
logger.warn("Received [#{code} #{reason}], [#{message}]")
|
288
|
+
|
289
|
+
true
|
290
|
+
end
|
291
|
+
|
292
|
+
def parse_notice(notice)
|
293
|
+
return if notice.object.nil?
|
294
|
+
|
295
|
+
pod = notice.object
|
296
|
+
container_ref = pod.metadata.name
|
297
|
+
container_state = pod.to_h[:status].deep_stringify_keys
|
298
|
+
|
299
|
+
{"container_ref" => container_ref, "container_state" => container_state}
|
300
|
+
end
|
301
|
+
|
302
|
+
def kubeclient
|
303
|
+
return @kubeclient unless @kubeclient.nil?
|
304
|
+
|
305
|
+
if server && token
|
306
|
+
api_endpoint = server
|
307
|
+
auth_options = {:bearer_token => token}
|
308
|
+
ssl_options = {:verify_ssl => verify_ssl}
|
309
|
+
ssl_options[:ca_file] = ca_file if ca_file
|
310
|
+
else
|
311
|
+
context = kubeconfig&.context(kubeconfig_context)
|
312
|
+
raise ArgumentError, "Missing connections options, provide a kubeconfig file or pass server and token via --docker-runner-options" if context.nil?
|
313
|
+
|
314
|
+
api_endpoint = context.api_endpoint
|
315
|
+
auth_options = context.auth_options
|
316
|
+
ssl_options = context.ssl_options
|
317
|
+
end
|
318
|
+
|
319
|
+
@kubeclient = Kubeclient::Client.new(api_endpoint, "v1", :ssl_options => ssl_options, :auth_options => auth_options).tap(&:discover)
|
320
|
+
end
|
321
|
+
|
322
|
+
def kubeconfig
|
323
|
+
return if kubeconfig_file.nil? || !File.exist?(kubeconfig_file)
|
324
|
+
|
325
|
+
Kubeclient::Config.read(kubeconfig_file)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class ContainerRunner
|
5
|
+
class Podman < Docker
|
6
|
+
DOCKER_COMMAND = "podman"
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
require "awesome_spawn"
|
10
|
+
require "securerandom"
|
11
|
+
|
12
|
+
super
|
13
|
+
|
14
|
+
@identity = options["identity"]
|
15
|
+
@log_level = options["log-level"]
|
16
|
+
@network = options["network"]
|
17
|
+
@noout = options["noout"].to_s == "true" if options.key?("noout")
|
18
|
+
@pull_policy = options["pull-policy"]
|
19
|
+
@root = options["root"]
|
20
|
+
@runroot = options["runroot"]
|
21
|
+
@runtime = options["runtime"]
|
22
|
+
@runtime_flag = options["runtime-flag"]
|
23
|
+
@storage_driver = options["storage-driver"]
|
24
|
+
@storage_opt = options["storage-opt"]
|
25
|
+
@syslog = options["syslog"].to_s == "true" if options.key?("syslog")
|
26
|
+
@tmpdir = options["tmpdir"]
|
27
|
+
@transient_store = !!options["transient-store"] if options.key?("transient-store")
|
28
|
+
@volumepath = options["volumepath"]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def run_container_params(image, env, secret)
|
34
|
+
params = ["run"]
|
35
|
+
params << :detach
|
36
|
+
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
37
|
+
params << [:e, "_CREDENTIALS=/run/secrets/#{secret}"] if secret
|
38
|
+
params << [:pull, @pull_policy] if @pull_policy
|
39
|
+
params << [:net, "host"] if @network == "host"
|
40
|
+
params << [:secret, secret] if secret
|
41
|
+
params << [:name, container_name(image)]
|
42
|
+
params << image
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_secret(secrets)
|
46
|
+
secret_guid = SecureRandom.uuid
|
47
|
+
podman!("secret", "create", secret_guid, "-", :in_data => secrets.to_json)
|
48
|
+
secret_guid
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete_secret(secret_guid)
|
52
|
+
podman!("secret", "rm", secret_guid)
|
53
|
+
rescue
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_notice(notice)
|
58
|
+
id, status, exit_code = JSON.parse(notice).values_at("ID", "Status", "ContainerExitCode")
|
59
|
+
|
60
|
+
event = podman_event_status_to_event(status)
|
61
|
+
running = event != :delete
|
62
|
+
|
63
|
+
runner_context = {"container_ref" => id, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
|
64
|
+
|
65
|
+
[event, runner_context]
|
66
|
+
rescue JSON::ParserError
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
|
70
|
+
def podman_event_status_to_event(status)
|
71
|
+
case status
|
72
|
+
when "create"
|
73
|
+
:create
|
74
|
+
when "init", "start"
|
75
|
+
:update
|
76
|
+
when "died", "cleanup", "remove"
|
77
|
+
:delete
|
78
|
+
else
|
79
|
+
:unknown
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
alias podman! docker!
|
84
|
+
|
85
|
+
def global_docker_options
|
86
|
+
options = []
|
87
|
+
options << [:identity, @identity] if @identity
|
88
|
+
options << [:"log-level", @log_level] if @log_level
|
89
|
+
options << :noout if @noout
|
90
|
+
options << [:root, @root] if @root
|
91
|
+
options << [:runroot, @runroot] if @runroot
|
92
|
+
options << [:runtime, @runtime] if @runtime
|
93
|
+
options << [:"runtime-flag", @runtime_flag] if @runtime_flag
|
94
|
+
options << [:"storage-driver", @storage_driver] if @storage_driver
|
95
|
+
options << [:"storage-opt", @storage_opt] if @storage_opt
|
96
|
+
options << :syslog if @syslog
|
97
|
+
options << [:tmpdir, @tmpdir] if @tmpdir
|
98
|
+
options << [:"transient-store", @transient_store] if @transient_store
|
99
|
+
options << [:volumepath, @volumepath] if @volumepath
|
100
|
+
options
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "container_runner/docker_mixin"
|
4
|
+
require_relative "container_runner/docker"
|
5
|
+
require_relative "container_runner/kubernetes"
|
6
|
+
require_relative "container_runner/podman"
|
7
|
+
|
8
|
+
module Floe
|
9
|
+
class ContainerRunner
|
10
|
+
class << self
|
11
|
+
def cli_options(optimist)
|
12
|
+
optimist.banner("")
|
13
|
+
optimist.banner("Container runner options:")
|
14
|
+
|
15
|
+
optimist.opt :container_runner, "Type of runner for docker container images (docker, podman, or kubernetes)", :type => :string, :short => 'r'
|
16
|
+
optimist.opt :container_runner_options, "Options to pass to the container runner", :type => :strings, :short => 'o'
|
17
|
+
|
18
|
+
optimist.opt :docker, "Use docker to run container images (short for --container-runner=docker)", :type => :boolean
|
19
|
+
optimist.opt :podman, "Use podman to run container images (short for --container-runner=podman)", :type => :boolean
|
20
|
+
optimist.opt :kubernetes, "Use kubernetes to run container images (short for --container-runner=kubernetes)", :type => :boolean
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolve_cli_options!(opts)
|
24
|
+
# shortcut support
|
25
|
+
opts[:container_runner] ||= "docker" if opts[:docker]
|
26
|
+
opts[:container_runner] ||= "podman" if opts[:podman]
|
27
|
+
opts[:container_runner] ||= "kubernetes" if opts[:kubernetes]
|
28
|
+
|
29
|
+
runner_options = opts[:container_runner_options].to_h { |opt| opt.split("=", 2) }
|
30
|
+
|
31
|
+
begin
|
32
|
+
set_runner(opts[:container_runner], runner_options)
|
33
|
+
rescue ArgumentError => e
|
34
|
+
Optimist.die(:container_runner, e.message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def runner
|
39
|
+
@runner || set_runner(nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_runner(name_or_instance, options = {})
|
43
|
+
@runner =
|
44
|
+
case name_or_instance
|
45
|
+
when "docker", nil
|
46
|
+
Floe::ContainerRunner::Docker.new(options)
|
47
|
+
when "podman"
|
48
|
+
Floe::ContainerRunner::Podman.new(options)
|
49
|
+
when "kubernetes"
|
50
|
+
Floe::ContainerRunner::Kubernetes.new(options)
|
51
|
+
when Floe::Runner
|
52
|
+
name_or_instance
|
53
|
+
else
|
54
|
+
raise ArgumentError, "container runner must be one of: docker, podman, kubernetes"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Floe::Runner.register_scheme("docker", -> { Floe::ContainerRunner.runner })
|
data/lib/floe/runner.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Runner
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
OUTPUT_MARKER = "__FLOE_OUTPUT__\n"
|
8
|
+
|
9
|
+
def initialize(_options = {})
|
10
|
+
end
|
11
|
+
|
12
|
+
@runners = {}
|
13
|
+
class << self
|
14
|
+
def register_scheme(scheme, klass_or_proc)
|
15
|
+
@runners[scheme] = klass_or_proc
|
16
|
+
end
|
17
|
+
|
18
|
+
private def resolve_scheme(scheme)
|
19
|
+
runner = @runners[scheme]
|
20
|
+
runner = @runners[scheme] = @runners[scheme].call if runner.is_a?(Proc)
|
21
|
+
runner
|
22
|
+
end
|
23
|
+
|
24
|
+
def for_resource(resource)
|
25
|
+
raise ArgumentError, "resource cannot be nil" if resource.nil?
|
26
|
+
|
27
|
+
scheme = resource.split("://").first
|
28
|
+
resolve_scheme(scheme) || raise(ArgumentError, "Invalid resource scheme [#{scheme}]")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Run a command asynchronously and create a runner_context
|
33
|
+
# @return [Hash] runner_context
|
34
|
+
def run_async!(_resource, _env = {}, _secrets = {}, _context = {})
|
35
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
36
|
+
end
|
37
|
+
|
38
|
+
# update the runner_context
|
39
|
+
# @param [Hash] runner_context (the value returned from run_async!)
|
40
|
+
# @return [void]
|
41
|
+
def status!(_runner_context)
|
42
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
43
|
+
end
|
44
|
+
|
45
|
+
# check runner_contet to determine if the task is still running or completed
|
46
|
+
# @param [Hash] runner_context (the value returned from run_async!)
|
47
|
+
# @return [Boolean] value if the task is still running
|
48
|
+
# true if the task is still running
|
49
|
+
# false if it has completed
|
50
|
+
def running?(_runner_context)
|
51
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
52
|
+
end
|
53
|
+
|
54
|
+
# For a non-running? task, check if it was successful
|
55
|
+
# @param [Hash] runner_context (the value returned from run_async!)
|
56
|
+
# @return [Boolean] value if the task is still running
|
57
|
+
# true if the task completed successfully
|
58
|
+
# false if the task had an error
|
59
|
+
def success?(_runner_context)
|
60
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
61
|
+
end
|
62
|
+
|
63
|
+
# For a successful task, return the output
|
64
|
+
# @param [Hash] runner_context (the value returned from run_async!)
|
65
|
+
# @return [String, Hash] output from task
|
66
|
+
def output(_runner_context)
|
67
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Cleanup runner context resources
|
71
|
+
# Called after a task is completed and the runner_context is no longer needed.
|
72
|
+
# @param [Hash] runner_context (the value returned from run_async!)
|
73
|
+
# @return [void]
|
74
|
+
def cleanup(_runner_context)
|
75
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
76
|
+
end
|
77
|
+
|
78
|
+
def wait(timeout: nil, events: %i[create update delete])
|
79
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/floe/version.rb
CHANGED
@@ -5,8 +5,10 @@ module Floe
|
|
5
5
|
class Context
|
6
6
|
# @param context [Json|Hash] (default, create another with input and execution params)
|
7
7
|
# @param input [Hash] (default: {})
|
8
|
-
def initialize(context = nil, input:
|
8
|
+
def initialize(context = nil, input: nil)
|
9
9
|
context = JSON.parse(context) if context.kind_of?(String)
|
10
|
+
|
11
|
+
input ||= {}
|
10
12
|
input = JSON.parse(input) if input.kind_of?(String)
|
11
13
|
|
12
14
|
@context = context || {}
|
@@ -25,9 +25,8 @@ module Floe
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def finish
|
28
|
-
input
|
29
|
-
context.output
|
30
|
-
context.next_state = end? ? nil : @next
|
28
|
+
input = process_input(context.input)
|
29
|
+
context.output = process_output(input, result)
|
31
30
|
super
|
32
31
|
end
|
33
32
|
|
@@ -18,7 +18,7 @@ module Floe
|
|
18
18
|
@next = payload["Next"]
|
19
19
|
@end = !!payload["End"]
|
20
20
|
@resource = payload["Resource"]
|
21
|
-
@runner = Floe::
|
21
|
+
@runner = Floe::Runner.for_resource(@resource)
|
22
22
|
@timeout_seconds = payload["TimeoutSeconds"]
|
23
23
|
@retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
|
24
24
|
@catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
|
@@ -36,15 +36,11 @@ module Floe
|
|
36
36
|
super
|
37
37
|
|
38
38
|
input = process_input(input)
|
39
|
-
runner_context = runner.run_async!(resource, input, credentials&.value({}, workflow.credentials))
|
39
|
+
runner_context = runner.run_async!(resource, input, credentials&.value({}, workflow.credentials), context)
|
40
40
|
|
41
41
|
context.state["RunnerContext"] = runner_context
|
42
42
|
end
|
43
43
|
|
44
|
-
def status
|
45
|
-
@end ? "success" : "running"
|
46
|
-
end
|
47
|
-
|
48
44
|
def finish
|
49
45
|
output = runner.output(context.state["RunnerContext"])
|
50
46
|
|
@@ -53,7 +49,6 @@ module Floe
|
|
53
49
|
context.output = process_output(context.input.dup, output)
|
54
50
|
super
|
55
51
|
else
|
56
|
-
context.next_state = nil
|
57
52
|
context.output = error = parse_error(output)
|
58
53
|
super
|
59
54
|
retry_state!(error) || catch_error!(error) || fail_workflow!(error)
|
data/lib/floe.rb
CHANGED
@@ -5,6 +5,8 @@ require_relative "floe/version"
|
|
5
5
|
require_relative "floe/null_logger"
|
6
6
|
require_relative "floe/logging"
|
7
7
|
|
8
|
+
require_relative "floe/runner"
|
9
|
+
|
8
10
|
require_relative "floe/workflow"
|
9
11
|
require_relative "floe/workflow/catcher"
|
10
12
|
require_relative "floe/workflow/choice_rule"
|
@@ -17,11 +19,6 @@ require_relative "floe/workflow/path"
|
|
17
19
|
require_relative "floe/workflow/payload_template"
|
18
20
|
require_relative "floe/workflow/reference_path"
|
19
21
|
require_relative "floe/workflow/retrier"
|
20
|
-
require_relative "floe/workflow/runner"
|
21
|
-
require_relative "floe/workflow/runner/docker_mixin"
|
22
|
-
require_relative "floe/workflow/runner/docker"
|
23
|
-
require_relative "floe/workflow/runner/kubernetes"
|
24
|
-
require_relative "floe/workflow/runner/podman"
|
25
22
|
require_relative "floe/workflow/state"
|
26
23
|
require_relative "floe/workflow/states/choice"
|
27
24
|
require_relative "floe/workflow/states/fail"
|
@@ -55,17 +52,4 @@ module Floe
|
|
55
52
|
def self.logger=(logger)
|
56
53
|
@logger = logger
|
57
54
|
end
|
58
|
-
|
59
|
-
# Set the runner to use
|
60
|
-
#
|
61
|
-
# @example
|
62
|
-
# Floe.set_runner "docker", kubernetes", {}
|
63
|
-
# Floe.set_runner "docker", Floe::Workflow::Runner::Kubernetes.new({})
|
64
|
-
#
|
65
|
-
# @param scheme [String] scheme Protocol to register (e.g.: docker)
|
66
|
-
# @param name_or_instance [String|Floe::Workflow::Runner] Name of runner to use for docker (e.g.: docker)
|
67
|
-
# @param options [Hash] Options for constructor of the runner (optional)
|
68
|
-
def self.set_runner(scheme, name_or_instance, options = {})
|
69
|
-
Floe::Workflow::Runner.set_runner(scheme, name_or_instance, options)
|
70
|
-
end
|
71
55
|
end
|