ood_core 0.12.0 → 0.16.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/.github/workflows/test.yml +30 -0
- data/CHANGELOG.md +63 -1
- data/README.md +2 -2
- data/lib/ood_core/job/adapters/ccq.rb +274 -0
- data/lib/ood_core/job/adapters/kubernetes.rb +193 -0
- data/lib/ood_core/job/adapters/kubernetes/batch.rb +372 -0
- data/lib/ood_core/job/adapters/kubernetes/helper.rb +299 -0
- data/lib/ood_core/job/adapters/kubernetes/k8s_job_info.rb +9 -0
- data/lib/ood_core/job/adapters/kubernetes/resources.rb +82 -0
- data/lib/ood_core/job/adapters/kubernetes/templates/pod.yml.erb +188 -0
- data/lib/ood_core/job/adapters/linux_host/launcher.rb +25 -10
- data/lib/ood_core/job/adapters/linux_host/templates/script_wrapper.erb.sh +3 -14
- data/lib/ood_core/job/adapters/slurm.rb +18 -1
- data/lib/ood_core/version.rb +1 -1
- data/ood_core.gemspec +2 -1
- metadata +33 -6
- data/.travis.yml +0 -9
@@ -0,0 +1,372 @@
|
|
1
|
+
require "ood_core/refinements/hash_extensions"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
class OodCore::Job::Adapters::Kubernetes::Batch
|
5
|
+
|
6
|
+
require_relative "helper"
|
7
|
+
require_relative "k8s_job_info"
|
8
|
+
|
9
|
+
using OodCore::Refinements::HashExtensions
|
10
|
+
|
11
|
+
class Error < StandardError; end
|
12
|
+
class NotFoundError < StandardError; end
|
13
|
+
|
14
|
+
attr_reader :config_file, :bin, :cluster, :mounts
|
15
|
+
attr_reader :all_namespaces, :using_context, :helper
|
16
|
+
attr_reader :username_prefix, :namespace_prefix
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
options = options.to_h.symbolize_keys
|
20
|
+
|
21
|
+
@config_file = options.fetch(:config_file, default_config_file)
|
22
|
+
@bin = options.fetch(:bin, '/usr/bin/kubectl')
|
23
|
+
@cluster = options.fetch(:cluster, 'open-ondemand')
|
24
|
+
@mounts = options.fetch(:mounts, []).map { |m| m.to_h.symbolize_keys }
|
25
|
+
@all_namespaces = options.fetch(:all_namespaces, false)
|
26
|
+
@username_prefix = options.fetch(:username_prefix, nil)
|
27
|
+
@namespace_prefix = options.fetch(:namespace_prefix, '')
|
28
|
+
|
29
|
+
@using_context = false
|
30
|
+
@helper = OodCore::Job::Adapters::Kubernetes::Helper.new
|
31
|
+
|
32
|
+
begin
|
33
|
+
make_kubectl_config(options)
|
34
|
+
rescue
|
35
|
+
# FIXME could use a log here
|
36
|
+
# means you couldn't 'kubectl set config'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def resource_file(resource_type = 'pod')
|
41
|
+
File.dirname(__FILE__) + "/templates/#{resource_type}.yml.erb"
|
42
|
+
end
|
43
|
+
|
44
|
+
def submit(script, after: [], afterok: [], afternotok: [], afterany: [])
|
45
|
+
raise ArgumentError, 'Must specify the script' if script.nil?
|
46
|
+
|
47
|
+
resource_yml, id = generate_id_yml(script)
|
48
|
+
call("#{formatted_ns_cmd} create -f -", stdin: resource_yml)
|
49
|
+
|
50
|
+
id
|
51
|
+
end
|
52
|
+
|
53
|
+
def generate_id(name)
|
54
|
+
# 2_821_109_907_456 = 36**8
|
55
|
+
name.downcase.tr(' ', '-') + '-' + rand(2_821_109_907_456).to_s(36)
|
56
|
+
end
|
57
|
+
|
58
|
+
def info_all(attrs: nil)
|
59
|
+
cmd = if all_namespaces
|
60
|
+
"#{base_cmd} get pods -o json --all-namespaces"
|
61
|
+
else
|
62
|
+
"#{namespaced_cmd} get pods -o json"
|
63
|
+
end
|
64
|
+
|
65
|
+
output = call(cmd)
|
66
|
+
all_pods_to_info(output)
|
67
|
+
end
|
68
|
+
|
69
|
+
def info_where_owner(owner, attrs: nil)
|
70
|
+
owner = Array.wrap(owner).map(&:to_s)
|
71
|
+
|
72
|
+
# must at least have job_owner to filter by job_owner
|
73
|
+
attrs = Array.wrap(attrs) | [:job_owner] unless attrs.nil?
|
74
|
+
|
75
|
+
info_all(attrs: attrs).select { |info| owner.include? info.job_owner }
|
76
|
+
end
|
77
|
+
|
78
|
+
def info_all_each(attrs: nil)
|
79
|
+
return to_enum(:info_all_each, attrs: attrs) unless block_given?
|
80
|
+
|
81
|
+
info_all(attrs: attrs).each do |job|
|
82
|
+
yield job
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def info_where_owner_each(owner, attrs: nil)
|
87
|
+
return to_enum(:info_where_owner_each, owner, attrs: attrs) unless block_given?
|
88
|
+
|
89
|
+
info_where_owner(owner, attrs: attrs).each do |job|
|
90
|
+
yield job
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def info(id)
|
95
|
+
pod_json = safe_call('get', 'pod', id)
|
96
|
+
return OodCore::Job::Info.new({ id: id, status: 'completed' }) if pod_json.empty?
|
97
|
+
|
98
|
+
service_json = safe_call('get', 'service', service_name(id))
|
99
|
+
secret_json = safe_call('get', 'secret', secret_name(id))
|
100
|
+
|
101
|
+
helper.info_from_json(pod_json: pod_json, service_json: service_json, secret_json: secret_json)
|
102
|
+
end
|
103
|
+
|
104
|
+
def status(id)
|
105
|
+
info(id).status
|
106
|
+
end
|
107
|
+
|
108
|
+
def delete(id)
|
109
|
+
safe_call("delete", "pod", id)
|
110
|
+
safe_call("delete", "service", service_name(id))
|
111
|
+
safe_call("delete", "secret", secret_name(id))
|
112
|
+
safe_call("delete", "configmap", configmap_name(id))
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def safe_call(verb, resource, id)
|
118
|
+
begin
|
119
|
+
case verb.to_s
|
120
|
+
when "get"
|
121
|
+
call_json_output('get', resource, id)
|
122
|
+
when "delete"
|
123
|
+
call("#{namespaced_cmd} delete #{resource} #{id}")
|
124
|
+
end
|
125
|
+
rescue NotFoundError
|
126
|
+
{}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# helper to help format multi-line yaml data from the submit.yml into
|
131
|
+
# mutli-line yaml in the pod.yml.erb
|
132
|
+
def config_data_lines(data)
|
133
|
+
output = []
|
134
|
+
first = true
|
135
|
+
|
136
|
+
data.to_s.each_line do |line|
|
137
|
+
output.append(first ? line : line.prepend(" "))
|
138
|
+
first = false
|
139
|
+
end
|
140
|
+
|
141
|
+
output
|
142
|
+
end
|
143
|
+
|
144
|
+
def username
|
145
|
+
@username ||= Etc.getlogin
|
146
|
+
end
|
147
|
+
|
148
|
+
def k8s_username
|
149
|
+
username_prefix.nil? ? username : "#{username_prefix}-#{username}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def user
|
153
|
+
@user ||= Etc.getpwnam(username)
|
154
|
+
end
|
155
|
+
|
156
|
+
def home_dir
|
157
|
+
user.dir
|
158
|
+
end
|
159
|
+
|
160
|
+
def run_as_user
|
161
|
+
user.uid
|
162
|
+
end
|
163
|
+
|
164
|
+
def run_as_group
|
165
|
+
user.gid
|
166
|
+
end
|
167
|
+
|
168
|
+
def fs_group
|
169
|
+
run_as_group
|
170
|
+
end
|
171
|
+
|
172
|
+
def group
|
173
|
+
Etc.getgrgid(run_as_group).name
|
174
|
+
end
|
175
|
+
|
176
|
+
def default_env
|
177
|
+
{
|
178
|
+
USER: username,
|
179
|
+
UID: run_as_user,
|
180
|
+
HOME: home_dir,
|
181
|
+
GROUP: group,
|
182
|
+
GID: run_as_group,
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
# helper to template resource yml you're going to submit and
|
187
|
+
# create an id.
|
188
|
+
def generate_id_yml(script)
|
189
|
+
native_data = script.native
|
190
|
+
container = helper.container_from_native(native_data[:container], default_env)
|
191
|
+
id = generate_id(container.name)
|
192
|
+
configmap = helper.configmap_from_native(native_data, id)
|
193
|
+
init_containers = helper.init_ctrs_from_native(native_data[:init_containers], container.env)
|
194
|
+
spec = OodCore::Job::Adapters::Kubernetes::Resources::PodSpec.new(container, init_containers: init_containers)
|
195
|
+
all_mounts = native_data[:mounts].nil? ? mounts : mounts + native_data[:mounts]
|
196
|
+
|
197
|
+
template = ERB.new(File.read(resource_file), nil, '-')
|
198
|
+
|
199
|
+
[template.result(binding), id]
|
200
|
+
end
|
201
|
+
|
202
|
+
# helper to call kubectl and get json data back.
|
203
|
+
# verb, resrouce and id are the kubernetes parlance terms.
|
204
|
+
# example: 'kubectl get pod my-pod-id' is verb=get, resource=pod
|
205
|
+
# and id=my-pod-id
|
206
|
+
def call_json_output(verb, resource, id, stdin: nil)
|
207
|
+
cmd = "#{formatted_ns_cmd} #{verb} #{resource} #{id}"
|
208
|
+
data = call(cmd, stdin: stdin)
|
209
|
+
data = data.empty? ? '{}' : data
|
210
|
+
json_data = JSON.parse(data, symbolize_names: true)
|
211
|
+
|
212
|
+
json_data
|
213
|
+
end
|
214
|
+
|
215
|
+
def service_name(id)
|
216
|
+
helper.service_name(id)
|
217
|
+
end
|
218
|
+
|
219
|
+
def secret_name(id)
|
220
|
+
helper.secret_name(id)
|
221
|
+
end
|
222
|
+
|
223
|
+
def configmap_name(id)
|
224
|
+
helper.configmap_name(id)
|
225
|
+
end
|
226
|
+
|
227
|
+
def namespace
|
228
|
+
"#{namespace_prefix}#{username}"
|
229
|
+
end
|
230
|
+
|
231
|
+
def context
|
232
|
+
cluster
|
233
|
+
end
|
234
|
+
|
235
|
+
def default_config_file
|
236
|
+
(ENV['KUBECONFIG'] || "#{Dir.home}/.kube/config")
|
237
|
+
end
|
238
|
+
|
239
|
+
def default_auth
|
240
|
+
{
|
241
|
+
type: 'managaged'
|
242
|
+
}.symbolize_keys
|
243
|
+
end
|
244
|
+
|
245
|
+
def default_server
|
246
|
+
{
|
247
|
+
endpoint: 'https://localhost:8080',
|
248
|
+
cert_authority_file: nil
|
249
|
+
}.symbolize_keys
|
250
|
+
end
|
251
|
+
|
252
|
+
def formatted_ns_cmd
|
253
|
+
"#{namespaced_cmd} -o json"
|
254
|
+
end
|
255
|
+
|
256
|
+
def namespaced_cmd
|
257
|
+
"#{base_cmd} --namespace=#{namespace}"
|
258
|
+
end
|
259
|
+
|
260
|
+
def base_cmd
|
261
|
+
base = "#{bin} --kubeconfig=#{config_file}"
|
262
|
+
base << " --context=#{context}" if using_context
|
263
|
+
base
|
264
|
+
end
|
265
|
+
|
266
|
+
def all_pods_to_info(data)
|
267
|
+
json_data = JSON.parse(data, symbolize_names: true)
|
268
|
+
pods = json_data.dig(:items)
|
269
|
+
|
270
|
+
info_array = []
|
271
|
+
pods.each do |pod|
|
272
|
+
info = pod_info_from_json(pod)
|
273
|
+
info_array.push(info) unless info.nil?
|
274
|
+
end
|
275
|
+
|
276
|
+
info_array
|
277
|
+
rescue JSON::ParserError
|
278
|
+
# 'no resources in <namespace>' throws parse error
|
279
|
+
[]
|
280
|
+
end
|
281
|
+
|
282
|
+
def pod_info_from_json(pod)
|
283
|
+
hash = helper.pod_info_from_json(pod)
|
284
|
+
K8sJobInfo.new(hash)
|
285
|
+
rescue Helper::K8sDataError
|
286
|
+
# FIXME: silently eating error, could probably use a logger
|
287
|
+
nil
|
288
|
+
end
|
289
|
+
|
290
|
+
def make_kubectl_config(config)
|
291
|
+
set_cluster(config.fetch(:server, default_server).to_h.symbolize_keys)
|
292
|
+
configure_auth(config.fetch(:auth, default_auth).to_h.symbolize_keys)
|
293
|
+
end
|
294
|
+
|
295
|
+
def configure_auth(auth)
|
296
|
+
type = auth.fetch(:type)
|
297
|
+
return if managed?(type)
|
298
|
+
|
299
|
+
case type
|
300
|
+
when 'gke'
|
301
|
+
set_gke_config(auth)
|
302
|
+
when 'oidc'
|
303
|
+
set_context
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def use_context
|
308
|
+
@using_context = true
|
309
|
+
end
|
310
|
+
|
311
|
+
def managed?(type)
|
312
|
+
if type.nil?
|
313
|
+
true # maybe should be false?
|
314
|
+
else
|
315
|
+
type.to_s == 'managed'
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def set_gke_config(auth)
|
320
|
+
cred_file = auth.fetch(:svc_acct_file)
|
321
|
+
|
322
|
+
cmd = "gcloud auth activate-service-account --key-file=#{cred_file}"
|
323
|
+
call(cmd)
|
324
|
+
|
325
|
+
set_gke_credentials(auth)
|
326
|
+
end
|
327
|
+
|
328
|
+
def set_gke_credentials(auth)
|
329
|
+
|
330
|
+
zone = auth.fetch(:zone, nil)
|
331
|
+
region = auth.fetch(:region, nil)
|
332
|
+
|
333
|
+
locale = ''
|
334
|
+
locale = "--zone=#{zone}" unless zone.nil?
|
335
|
+
locale = "--region=#{region}" unless region.nil?
|
336
|
+
|
337
|
+
# gke cluster name can probably can differ from what ood calls the cluster
|
338
|
+
cmd = "gcloud container clusters get-credentials #{locale} #{cluster}"
|
339
|
+
env = { 'KUBECONFIG' => config_file }
|
340
|
+
call(cmd, env)
|
341
|
+
end
|
342
|
+
|
343
|
+
def set_context
|
344
|
+
cmd = "#{base_cmd} config set-context #{cluster}"
|
345
|
+
cmd << " --cluster=#{cluster} --namespace=#{namespace}"
|
346
|
+
cmd << " --user=#{k8s_username}"
|
347
|
+
|
348
|
+
call(cmd)
|
349
|
+
use_context
|
350
|
+
end
|
351
|
+
|
352
|
+
def set_cluster(config)
|
353
|
+
server = config.fetch(:endpoint)
|
354
|
+
cert = config.fetch(:cert_authority_file, nil)
|
355
|
+
|
356
|
+
cmd = "#{base_cmd} config set-cluster #{cluster}"
|
357
|
+
cmd << " --server=#{server}"
|
358
|
+
cmd << " --certificate-authority=#{cert}" unless cert.nil?
|
359
|
+
|
360
|
+
call(cmd)
|
361
|
+
end
|
362
|
+
|
363
|
+
def call(cmd = '', env: {}, stdin: nil)
|
364
|
+
o, e, s = Open3.capture3(env, cmd, stdin_data: stdin.to_s)
|
365
|
+
s.success? ? o : interpret_and_raise(e)
|
366
|
+
end
|
367
|
+
|
368
|
+
def interpret_and_raise(stderr)
|
369
|
+
raise NotFoundError, stderr if /^Error from server \(NotFound\):/.match(stderr)
|
370
|
+
raise(Error, stderr)
|
371
|
+
end
|
372
|
+
end
|
@@ -0,0 +1,299 @@
|
|
1
|
+
class OodCore::Job::Adapters::Kubernetes::Helper
|
2
|
+
|
3
|
+
require_relative 'resources'
|
4
|
+
require_relative 'k8s_job_info'
|
5
|
+
require 'resolv'
|
6
|
+
require 'base64'
|
7
|
+
require 'active_support/core_ext/hash'
|
8
|
+
|
9
|
+
class K8sDataError < StandardError; end
|
10
|
+
|
11
|
+
# Extract info from json data. The data is expected to be from the kubectl
|
12
|
+
# command and conform to kubernetes' datatype structures.
|
13
|
+
#
|
14
|
+
# Returns K8sJobInfo in the in lieu of writing a connection.yml
|
15
|
+
#
|
16
|
+
# @param pod_json [#to_h]
|
17
|
+
# the pod data returned from 'kubectl get pod abc-123'
|
18
|
+
# @param service_json [#to_h]
|
19
|
+
# the service data returned from 'kubectl get service abc-123-service'
|
20
|
+
# @param secret_json [#to_h]
|
21
|
+
# the secret data returned from 'kubectl get secret abc-123-secret'
|
22
|
+
# @param ns_prefix [#to_s]
|
23
|
+
# the namespace prefix so that namespaces can be converted back to usernames
|
24
|
+
# @return [OodCore::Job::Adapters::Kubernetes::K8sJobInfo]
|
25
|
+
def info_from_json(pod_json: nil, service_json: nil, secret_json: nil, ns_prefix: nil)
|
26
|
+
pod_hash = pod_info_from_json(pod_json, ns_prefix: ns_prefix)
|
27
|
+
service_hash = service_info_from_json(service_json)
|
28
|
+
secret_hash = secret_info_from_json(secret_json)
|
29
|
+
|
30
|
+
pod_hash.deep_merge!(service_hash)
|
31
|
+
pod_hash.deep_merge!(secret_hash)
|
32
|
+
OodCore::Job::Adapters::Kubernetes::K8sJobInfo.new(pod_hash)
|
33
|
+
rescue NoMethodError
|
34
|
+
raise K8sDataError, "unable to read data correctly from json"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Turn a container hash into a Kubernetes::Resources::Container
|
38
|
+
#
|
39
|
+
# @param container [#to_h]
|
40
|
+
# the input container hash
|
41
|
+
# @param default_env [#to_h]
|
42
|
+
# Default env to merge with defined env
|
43
|
+
# @return [OodCore::Job::Adapters::Kubernetes::Resources::Container]
|
44
|
+
def container_from_native(container, default_env)
|
45
|
+
env = container.fetch(:env, {}).to_h.symbolize_keys
|
46
|
+
OodCore::Job::Adapters::Kubernetes::Resources::Container.new(
|
47
|
+
container[:name],
|
48
|
+
container[:image],
|
49
|
+
command: parse_command(container[:command]),
|
50
|
+
port: container[:port],
|
51
|
+
env: default_env.merge(env),
|
52
|
+
memory: container[:memory],
|
53
|
+
cpu: container[:cpu],
|
54
|
+
working_dir: container[:working_dir],
|
55
|
+
restart_policy: container[:restart_policy],
|
56
|
+
image_pull_secret: container[:image_pull_secret]
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parse a command string given from a user and return an array.
|
61
|
+
# If given an array, the input is simply returned back.
|
62
|
+
#
|
63
|
+
# @param cmd [#to_s]
|
64
|
+
# the command to parse
|
65
|
+
# @return [Array<#to_s>]
|
66
|
+
# the command parsed into an array of arguements
|
67
|
+
def parse_command(cmd)
|
68
|
+
if cmd&.is_a?(Array)
|
69
|
+
cmd
|
70
|
+
else
|
71
|
+
Shellwords.split(cmd.to_s)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Turn a configmap hash into a Kubernetes::Resources::ConfigMap
|
76
|
+
# that can be used in templates. Needs an id so that the resulting
|
77
|
+
# configmap has a known name.
|
78
|
+
#
|
79
|
+
# @param native [#to_h]
|
80
|
+
# the input configmap hash
|
81
|
+
# @param id [#to_s]
|
82
|
+
# the id to use for giving the configmap a name
|
83
|
+
# @return [OodCore::Job::Adapters::Kubernetes::Resources::ConfigMap]
|
84
|
+
def configmap_from_native(native, id)
|
85
|
+
configmap = native.fetch(:configmap, nil)
|
86
|
+
return nil if configmap.nil?
|
87
|
+
|
88
|
+
OodCore::Job::Adapters::Kubernetes::Resources::ConfigMap.new(
|
89
|
+
configmap_name(id),
|
90
|
+
(configmap[:files] || [])
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
# parse initialization containers from native data
|
95
|
+
#
|
96
|
+
# @param native_data [#to_h]
|
97
|
+
# the native data to parse. Expected key init_ctrs and for that
|
98
|
+
# key to be an array of hashes.
|
99
|
+
# @param default_env [#to_h]
|
100
|
+
# Default env to merge with defined env
|
101
|
+
# @return [Array<OodCore::Job::Adapters::Kubernetes::Resources::Container>]
|
102
|
+
# the array of init containers
|
103
|
+
def init_ctrs_from_native(ctrs, default_env)
|
104
|
+
init_ctrs = []
|
105
|
+
|
106
|
+
ctrs&.each do |ctr_raw|
|
107
|
+
ctr = container_from_native(ctr_raw, default_env)
|
108
|
+
init_ctrs.push(ctr)
|
109
|
+
end
|
110
|
+
|
111
|
+
init_ctrs
|
112
|
+
end
|
113
|
+
|
114
|
+
def service_name(id)
|
115
|
+
id + '-service'
|
116
|
+
end
|
117
|
+
|
118
|
+
def secret_name(id)
|
119
|
+
id + '-secret'
|
120
|
+
end
|
121
|
+
|
122
|
+
def configmap_name(id)
|
123
|
+
id + '-configmap'
|
124
|
+
end
|
125
|
+
|
126
|
+
def seconds_to_duration(s)
|
127
|
+
"%02dh%02dm%02ds" % [s / 3600, s / 60 % 60, s % 60]
|
128
|
+
end
|
129
|
+
|
130
|
+
# Extract pod info from json data. The data is expected to be from the kubectl
|
131
|
+
# command and conform to kubernetes' datatype structures.
|
132
|
+
#
|
133
|
+
# @param json_data [#to_h]
|
134
|
+
# the pod data returned from 'kubectl get pod abc-123'
|
135
|
+
# @param ns_prefix [#to_s]
|
136
|
+
# the namespace prefix so that namespaces can be converted back to usernames
|
137
|
+
# @return [#to_h]
|
138
|
+
# the hash of info expected from adapters
|
139
|
+
def pod_info_from_json(json_data, ns_prefix: nil)
|
140
|
+
{
|
141
|
+
id: json_data.dig(:metadata, :name).to_s,
|
142
|
+
job_name: name_from_metadata(json_data.dig(:metadata)),
|
143
|
+
status: pod_status_from_json(json_data),
|
144
|
+
job_owner: job_owner_from_json(json_data, ns_prefix),
|
145
|
+
submission_time: submission_time(json_data),
|
146
|
+
dispatch_time: dispatch_time(json_data),
|
147
|
+
wallclock_time: wallclock_time(json_data),
|
148
|
+
ood_connection_info: { host: get_host(json_data.dig(:status, :hostIP)) },
|
149
|
+
procs: procs_from_json(json_data)
|
150
|
+
}
|
151
|
+
rescue NoMethodError
|
152
|
+
# gotta raise an error because Info.new will throw an error if id is undefined
|
153
|
+
raise K8sDataError, "unable to read data correctly from json"
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def get_host(ip)
|
159
|
+
Resolv.getname(ip)
|
160
|
+
rescue Resolv::ResolvError
|
161
|
+
ip
|
162
|
+
end
|
163
|
+
|
164
|
+
def name_from_metadata(metadata)
|
165
|
+
name = metadata.dig(:labels, :'app.kubernetes.io/name')
|
166
|
+
name = metadata.dig(:labels, :'k8s-app') if name.nil?
|
167
|
+
name = metadata.dig(:name) if name.nil? # pod-id but better than nil?
|
168
|
+
name
|
169
|
+
end
|
170
|
+
|
171
|
+
def service_info_from_json(json_data)
|
172
|
+
# all we need is the port - .spec.ports[0].nodePort
|
173
|
+
ports = json_data.dig(:spec, :ports)
|
174
|
+
{ ood_connection_info: { port: ports[0].dig(:nodePort) } }
|
175
|
+
rescue
|
176
|
+
{}
|
177
|
+
end
|
178
|
+
|
179
|
+
def secret_info_from_json(json_data)
|
180
|
+
raw = json_data.dig(:data, :password)
|
181
|
+
{ ood_connection_info: { password: Base64.decode64(raw) } }
|
182
|
+
rescue
|
183
|
+
{}
|
184
|
+
end
|
185
|
+
|
186
|
+
def dispatch_time(json_data)
|
187
|
+
status = pod_status_from_json(json_data)
|
188
|
+
container_statuses = json_data.dig(:status, :containerStatuses)
|
189
|
+
return nil if container_statuses.nil?
|
190
|
+
|
191
|
+
state_data = container_statuses[0].dig(:state)
|
192
|
+
date_string = nil
|
193
|
+
|
194
|
+
if status == 'completed'
|
195
|
+
date_string = state_data.dig(:terminated, :startedAt)
|
196
|
+
elsif status == 'running'
|
197
|
+
date_string = state_data.dig(:running, :startedAt)
|
198
|
+
end
|
199
|
+
|
200
|
+
date_string.nil? ? nil : DateTime.parse(date_string).to_time.to_i
|
201
|
+
end
|
202
|
+
|
203
|
+
def wallclock_time(json_data)
|
204
|
+
status = pod_status_from_json(json_data)
|
205
|
+
container_statuses = json_data.dig(:status, :containerStatuses)
|
206
|
+
return nil if container_statuses.nil?
|
207
|
+
|
208
|
+
state_data = container_statuses[0].dig(:state)
|
209
|
+
start_time = dispatch_time(json_data)
|
210
|
+
return nil if start_time.nil?
|
211
|
+
|
212
|
+
et = end_time(status, state_data)
|
213
|
+
|
214
|
+
et.nil? ? nil : et - start_time
|
215
|
+
end
|
216
|
+
|
217
|
+
def end_time(status, state_data)
|
218
|
+
if status == 'completed'
|
219
|
+
end_time_string = state_data.dig(:terminated, :finishedAt)
|
220
|
+
et = DateTime.parse(end_time_string).to_time.to_i
|
221
|
+
elsif status == 'running'
|
222
|
+
et = DateTime.now.to_time.to_i
|
223
|
+
else
|
224
|
+
et = nil
|
225
|
+
end
|
226
|
+
|
227
|
+
et
|
228
|
+
end
|
229
|
+
|
230
|
+
def submission_time(json_data)
|
231
|
+
status = json_data.dig(:status)
|
232
|
+
start = status.dig(:startTime)
|
233
|
+
|
234
|
+
if start.nil?
|
235
|
+
# the pod is in some pending state limbo
|
236
|
+
conditions = status.dig(:conditions)
|
237
|
+
# best guess to start time is just the first condition's
|
238
|
+
# transition time
|
239
|
+
str = conditions[0].dig(:lastTransitionTime)
|
240
|
+
else
|
241
|
+
str = start
|
242
|
+
end
|
243
|
+
|
244
|
+
DateTime.parse(str).to_time.to_i
|
245
|
+
end
|
246
|
+
|
247
|
+
def pod_status_from_json(json_data)
|
248
|
+
phase = json_data.dig(:status, :phase)
|
249
|
+
state = case phase
|
250
|
+
when "Running"
|
251
|
+
"running"
|
252
|
+
when "Pending"
|
253
|
+
"queued"
|
254
|
+
when "Failed"
|
255
|
+
"suspended"
|
256
|
+
when "Succeeded"
|
257
|
+
"completed"
|
258
|
+
when "Unknown"
|
259
|
+
"undetermined"
|
260
|
+
else
|
261
|
+
"undetermined"
|
262
|
+
end
|
263
|
+
|
264
|
+
OodCore::Job::Status.new(state: state)
|
265
|
+
end
|
266
|
+
|
267
|
+
def terminated_state(status)
|
268
|
+
reason = status.dig(:terminated, :reason)
|
269
|
+
if reason == 'Error'
|
270
|
+
'suspended'
|
271
|
+
else
|
272
|
+
'completed'
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def procs_from_json(json_data)
|
277
|
+
containers = json_data.dig(:spec, :containers)
|
278
|
+
resources = containers[0].dig(:resources)
|
279
|
+
|
280
|
+
cpu = resources.dig(:limits, :cpu)
|
281
|
+
millicores_rex = /(\d+)m/
|
282
|
+
|
283
|
+
# ok to return string bc nil.to_i == 0 and we'd rather return
|
284
|
+
# nil (undefined) than 0 which is confusing.
|
285
|
+
if millicores_rex.match?(cpu)
|
286
|
+
millicores = millicores_rex.match(cpu)[1].to_i
|
287
|
+
|
288
|
+
# have to return at least 1 bc 200m could be 0
|
289
|
+
((millicores + 1000) / 1000).to_s
|
290
|
+
else
|
291
|
+
cpu
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def job_owner_from_json(json_data = {}, ns_prefix = nil)
|
296
|
+
namespace = json_data.dig(:metadata, :namespace).to_s
|
297
|
+
namespace.delete_prefix(ns_prefix.to_s)
|
298
|
+
end
|
299
|
+
end
|