ood_core 0.13.0 → 0.16.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3296708d7bc47f3379a9e4a6c845d3f25c5ccefb599f4b92406d9dffdaef220b
4
- data.tar.gz: b6af9e90b67bc9a7a52203808d849d8800336b30b09bdb8ed204526d01bc92e9
3
+ metadata.gz: 19665b6db28d01da39093dc90d4a5023ca12264f07b932aebc8ec8c443bafa25
4
+ data.tar.gz: d9c8c6d8f30851ea9138c8325aafd750823534a51f36601a20366265ac4feec2
5
5
  SHA512:
6
- metadata.gz: 623ac6e6f8081d68a3e925d1150c9f20a0f613ccfb6837519d1b95d04533a72caa403c54327aad85dcea9c0694cc23941f40307d942623c095f53fed7fc32026
7
- data.tar.gz: 0d785a9ade36b2f6f62f9ae55672091346aa4fb76bf358e6c00d4bc007623b8d1798813474665fc7b4d850d89e041fae5c2fefc9719fbe9f53a161a76127eaad
6
+ metadata.gz: 1ed1eaa873366ad5e825ed29c7401dd3bca4a424ab7a689a19479f297ec20d7e019cd53609006b0919a365dd0002eb0c1e9c0cabcc9f69579cf7ae81b33b3ae7
7
+ data.tar.gz: 90a4cfa3ee8b1f76ef7e1f28df6d8e64725d1eaff005b4bd4ff7fc8f88e5bfda8a15e706636c18e7b5ac74451071eaea4e6814945ea25e95f6c7ed2de8fd2fec
@@ -0,0 +1,30 @@
1
+ name: Unit Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ branches:
9
+ - master
10
+
11
+ jobs:
12
+ tests:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: checkout
17
+ uses: actions/checkout@v2
18
+
19
+ - name: Setup Ruby using Bundler
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: "2.7.1"
23
+ bundler-cache: true
24
+ bundler: "2.1.4"
25
+
26
+ - name: install gems
27
+ run: bundle install
28
+
29
+ - name: test
30
+ run: bundle exec rake spec
data/CHANGELOG.md CHANGED
@@ -6,6 +6,64 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
+ ## [0.16.1] - 2021-04-23
10
+ ### Fixed
11
+ - memorized some allow? variables to have better support around ACLS in
12
+ [267](https://github.com/OSC/ood_core/pull/267)
13
+
14
+ ## [0.16.0] - 2021-04-20
15
+ ### Fixed
16
+ - tmux 2.7+ bug in the linux host adapter in [2.5.8](https://github.com/OSC/ood_core/pull/258)
17
+ and [259](https://github.com/OSC/ood_core/pull/259).
18
+
19
+ ### Changed
20
+
21
+ - Changed how k8s configmaps in are defined in [251](https://github.com/OSC/ood_core/pull/251).
22
+ The data structure now expects a key called files which is an array of objects that hold
23
+ filename, data, mount_path, sub_path and init_mount_path.
24
+ [255](https://github.com/OSC/ood_core/pull/255) also relates to this interface change.
25
+
26
+ ### Added
27
+
28
+ - The k8s adapter can now specify environment variables and creates defaults
29
+ in [252](https://github.com/OSC/ood_core/pull/252).
30
+ - The k8s adapter can now specify image pull secrets in [253](https://github.com/OSC/ood_core/pull/253).
31
+
32
+ ## [0.15.1] - 2021-02-25
33
+ ### Fixed
34
+ - kubernetes adapter uses the full module for helpers in [245](https://github.com/OSC/ood_core/pull/245).
35
+
36
+ ### Changed
37
+ - kubernetes pods spawn with runAsNonRoot set to true in [247](https://github.com/OSC/ood_core/pull/247).
38
+ - kubernetes pods can spawn with supplemental groups along with some other in security defaults in
39
+ [246](https://github.com/OSC/ood_core/pull/246).
40
+
41
+ ## [0.15.0] - 2021-01-26
42
+ ### Fixed
43
+ - ccq adapter now accepts job names with spaces in [210](https://github.com/OSC/ood_core/pull/209)
44
+ - k8s correctly handles having no mount volumes in [239](https://github.com/OSC/ood_core/pull/239)
45
+
46
+ ### Added
47
+ - k8s adapter now applies account metadata to resources in [216](https://github.com/OSC/ood_core/pull/216) and
48
+ [231](https://github.com/OSC/ood_core/pull/231)
49
+ - k8s adapter can now prefix namespaces in [218](https://github.com/OSC/ood_core/pull/218)
50
+ - k8s adapter now applies time limits to pods in [224](https://github.com/OSC/ood_core/pull/224)
51
+
52
+ ### Changed
53
+ - testing automation is now done in github actions in [221](https://github.com/OSC/ood_core/pull/218)
54
+ - update bunlder to 2.1.4 and ruby to 2.7 in [235](https://github.com/OSC/ood_core/pull/218) updated bundler and ruby
55
+ - k8s adapter more appropriately labels unschedulable pods as queued in [230](https://github.com/OSC/ood_core/pull/230)
56
+ - k8s adapter now uses the script#ood_connection_info API instead of script#native in
57
+ [222](https://github.com/OSC/ood_core/pull/222)
58
+
59
+ ## [0.14.0] - 2020-10-01
60
+ ### Added
61
+ - Kubernetes adapter in PR [156](https://github.com/OSC/ood_core/pull/156)
62
+
63
+ ### Fixed
64
+ - Catch Slurm times. [209](https://github.com/OSC/ood_core/pull/209)
65
+ - LHA race condition in deleteing tmp files. [212](https://github.com/OSC/ood_core/pull/212)
66
+
9
67
  ## [0.13.0] - 2020-08-10
10
68
  ### Added
11
69
  - CloudyCluster CCQ Adapter
@@ -247,7 +305,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
247
305
  ### Added
248
306
  - Initial release!
249
307
 
250
- [Unreleased]: https://github.com/OSC/ood_core/compare/v0.13.0...HEAD
308
+ [Unreleased]: https://github.com/OSC/ood_core/compare/v0.16.1...HEAD
309
+ [0.16.1]: https://github.com/OSC/ood_core/compare/v0.16.0...v0.16.1
310
+ [0.16.0]: https://github.com/OSC/ood_core/compare/v0.15.1...v0.16.0
311
+ [0.15.1]: https://github.com/OSC/ood_core/compare/v0.15.0...v0.15.1
312
+ [0.15.0]: https://github.com/OSC/ood_core/compare/v0.14.0...v0.15.0
313
+ [0.14.0]: https://github.com/OSC/ood_core/compare/v0.13.0...v0.14.0
251
314
  [0.13.0]: https://github.com/OSC/ood_core/compare/v0.12.0...v0.13.0
252
315
  [0.12.0]: https://github.com/OSC/ood_core/compare/v0.11.4...v0.12.0
253
316
  [0.11.4]: https://github.com/OSC/ood_core/compare/v0.11.3...v0.11.4
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # OodCore
2
2
 
3
- [![Build Status](https://travis-ci.org/OSC/ood_core.svg?branch=master)](https://travis-ci.org/OSC/ood_core)
3
+ [![Build Status](https://github.com/osc/ood_core/workflows/Unit%20Tests/badge.svg)](https://github.com/OSC/ood_core/actions?query=workflow%3A%22Unit+Tests%22)
4
4
  ![GitHub Release](https://img.shields.io/github/release/osc/ood_core.svg)
5
5
  ![GitHub License](https://img.shields.io/github/license/osc/ood_core.svg)
6
6
 
7
7
  - Website: http://openondemand.org/
8
8
  - Website repo with JOSS publication: https://github.com/OSC/Open-OnDemand
9
- - Documentation: https://osc.github.io/ood-documentation/master/
9
+ - Documentation: https://osc.github.io/ood-documentation/latest/
10
10
  - Main code repo: https://github.com/OSC/ondemand
11
11
  - Core library repo: https://github.com/OSC/ood_core
12
12
 
@@ -78,7 +78,9 @@ module OodCore
78
78
  # Whether the login feature is allowed
79
79
  # @return [Boolean] is login allowed
80
80
  def login_allow?
81
- allow? && !login_config.empty?
81
+ return @login_allow if defined?(@login_allow)
82
+
83
+ @login_allow = (allow? && !login_config.empty?)
82
84
  end
83
85
 
84
86
  # Build a job adapter from the job configuration
@@ -90,9 +92,11 @@ module OodCore
90
92
  # Whether the job feature is allowed based on the ACLs
91
93
  # @return [Boolean] is the job feature allowed
92
94
  def job_allow?
93
- allow? &&
94
- !job_config.empty? &&
95
- build_acls(job_config.fetch(:acls, []).map(&:to_h)).all?(&:allow?)
95
+ return @job_allow if defined?(@job_allow)
96
+
97
+ @job_allow = (allow? && ! job_config.empty? && build_acls(
98
+ job_config.fetch(:acls, []).map(&:to_h)
99
+ ).all?(&:allow?))
96
100
  end
97
101
 
98
102
  # The batch connect template configuration used for this cluster
@@ -138,7 +142,9 @@ module OodCore
138
142
  # Whether this cluster is allowed to be used
139
143
  # @return [Boolean] whether cluster is allowed
140
144
  def allow?
141
- acls.all?(&:allow?)
145
+ return @allow if defined?(@allow)
146
+
147
+ @allow = acls.all?(&:allow?)
142
148
  end
143
149
 
144
150
  # The comparison operator
@@ -203,6 +203,10 @@ module OodCore
203
203
  'ccq_ood_script_'
204
204
  end
205
205
 
206
+ def ccqstat_regex
207
+ /^(?<id>\S+)\s+(?<name>.+)\s+(?<username>\S+)\s+(?<scheduler>\S+)\s+(?<status>\S+)\s*$/
208
+ end
209
+
206
210
  def parse_job_id_from_ccqsub(output)
207
211
  match_data = /#{jobid_regex}/.match(output)
208
212
  # match_data could be nil, OR re-configured jobid_regex could be looking for a different named group
@@ -236,28 +240,31 @@ module OodCore
236
240
  def info_from_ccqstat(data)
237
241
  infos = []
238
242
 
239
- data.to_s.each_line do |line|
240
- words = line.split(/\s/).reject(&:empty?)
241
- next if !words.empty? && words[0] == "Id" # just skip the header
242
-
243
- infos << Info.new(line_to_hash(words)) if words.size == 5
243
+ data.to_s.lines.drop(1).each do |line|
244
+ match_data = ccqstat_regex.match(line)
245
+ infos << Info.new(ccqstat_match_to_hash(match_data)) if valid_ccqstat_match?(match_data)
244
246
  end
245
247
 
246
248
  infos
247
249
  end
248
250
 
249
- def line_to_hash(words)
250
- return unless words.size == 5
251
-
251
+ def ccqstat_match_to_hash(match)
252
252
  data_hash = {}
253
- data_hash[:id] = words[0]
254
- data_hash[:job_name] = words[1]
255
- data_hash[:job_owner] = words[2]
256
- data_hash[:status] = get_state(words[4])
253
+ data_hash[:id] = match.named_captures.fetch('id', nil)
254
+ data_hash[:job_owner] = match.named_captures.fetch('username', nil)
255
+ data_hash[:status] = get_state(match.named_captures.fetch('status', nil))
256
+
257
+ # The regex leaves trailing empty spaces. There's no way to tell if they're _actually_
258
+ # a part of the job name or not, so we assume they're not and add the rstrip.
259
+ data_hash[:job_name] = match.named_captures.fetch('name', nil).to_s.rstrip
257
260
 
258
261
  data_hash
259
262
  end
260
263
 
264
+ def valid_ccqstat_match?(match)
265
+ !match.nil? && !match.named_captures.fetch('id', nil).nil?
266
+ end
267
+
261
268
  def get_state(state)
262
269
  STATE_MAP.fetch(state, :undetermined)
263
270
  end
@@ -0,0 +1,193 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+ require "ood_core/refinements/array_extensions"
3
+
4
+ module OodCore
5
+ module Job
6
+ class Factory
7
+ using Refinements::HashExtensions
8
+
9
+ def self.build_kubernetes(config)
10
+ batch = Adapters::Kubernetes::Batch.new(config.to_h.symbolize_keys)
11
+ Adapters::Kubernetes.new(batch)
12
+ end
13
+ end
14
+
15
+ module Adapters
16
+ class Kubernetes < Adapter
17
+
18
+ using Refinements::ArrayExtensions
19
+ using Refinements::HashExtensions
20
+
21
+ require "ood_core/job/adapters/kubernetes/batch"
22
+
23
+ attr_reader :batch
24
+
25
+ def initialize(batch)
26
+ @batch = batch
27
+ end
28
+
29
+ # Submit a job with the attributes defined in the job template instance
30
+ # @abstract Subclass is expected to implement {#submit}
31
+ # @raise [NotImplementedError] if subclass did not define {#submit}
32
+ # @example Submit job template to cluster
33
+ # solver_id = job_adapter.submit(solver_script)
34
+ # #=> "1234.server"
35
+ # @example Submit job that depends on previous job
36
+ # post_id = job_adapter.submit(
37
+ # post_script,
38
+ # afterok: solver_id
39
+ # )
40
+ # #=> "1235.server"
41
+ # @param script [Script] script object that describes the
42
+ # script and attributes for the submitted job
43
+ # @param after [#to_s, Array<#to_s>] this job may be scheduled for execution
44
+ # at any point after dependent jobs have started execution
45
+ # @param afterok [#to_s, Array<#to_s>] this job may be scheduled for
46
+ # execution only after dependent jobs have terminated with no errors
47
+ # @param afternotok [#to_s, Array<#to_s>] this job may be scheduled for
48
+ # execution only after dependent jobs have terminated with errors
49
+ # @param afterany [#to_s, Array<#to_s>] this job may be scheduled for
50
+ # execution after dependent jobs have terminated
51
+ # @return [String] the job id returned after successfully submitting a job
52
+ def submit(script, after: [], afterok: [], afternotok: [], afterany: [])
53
+ raise ArgumentError, 'Must specify the script' if script.nil?
54
+
55
+ batch.submit(script)
56
+ rescue Batch::Error => e
57
+ raise JobAdapterError, e.message
58
+ end
59
+
60
+
61
+ # Retrieve info for all jobs from the resource manager
62
+ # @abstract Subclass is expected to implement {#info_all}
63
+ # @raise [NotImplementedError] if subclass did not define {#info_all}
64
+ # @param attrs [Array<symbol>] defaults to nil (and all attrs are provided)
65
+ # This array specifies only attrs you want, in addition to id and status.
66
+ # If an array, the Info object that is returned to you is not guarenteed
67
+ # to have a value for any attr besides the ones specified and id and status.
68
+ #
69
+ # For certain adapters this may speed up the response since
70
+ # adapters can get by without populating the entire Info object
71
+ # @return [Array<Info>] information describing submitted jobs
72
+ def info_all(attrs: nil)
73
+ batch.info_all(attrs: attrs)
74
+ rescue Batch::Error => e
75
+ raise JobAdapterError, e.message
76
+ end
77
+
78
+ # Retrieve info for all jobs for a given owner or owners from the
79
+ # resource manager
80
+ # @param owner [#to_s, Array<#to_s>] the owner(s) of the jobs
81
+ # @param attrs [Array<symbol>] defaults to nil (and all attrs are provided)
82
+ # This array specifies only attrs you want, in addition to id and status.
83
+ # If an array, the Info object that is returned to you is not guarenteed
84
+ # to have a value for any attr besides the ones specified and id and status.
85
+ #
86
+ # For certain adapters this may speed up the response since
87
+ # adapters can get by without populating the entire Info object
88
+ # @return [Array<Info>] information describing submitted jobs
89
+ def info_where_owner(owner, attrs: nil)
90
+ owner = Array.wrap(owner).map(&:to_s)
91
+
92
+ # must at least have job_owner to filter by job_owner
93
+ attrs = Array.wrap(attrs) | [:job_owner] unless attrs.nil?
94
+
95
+ info_all(attrs: attrs).select { |info| owner.include? info.job_owner }
96
+ end
97
+
98
+ # Iterate over each job Info object
99
+ # @param attrs [Array<symbol>] defaults to nil (and all attrs are provided)
100
+ # This array specifies only attrs you want, in addition to id and status.
101
+ # If an array, the Info object that is returned to you is not guarenteed
102
+ # to have a value for any attr besides the ones specified and id and status.
103
+ #
104
+ # For certain adapters this may speed up the response since
105
+ # adapters can get by without populating the entire Info object
106
+ # @yield [Info] of each job to block
107
+ # @return [Enumerator] if no block given
108
+ def info_all_each(attrs: nil)
109
+ return to_enum(:info_all_each, attrs: attrs) unless block_given?
110
+
111
+ info_all(attrs: attrs).each do |job|
112
+ yield job
113
+ end
114
+ end
115
+
116
+ # Iterate over each job Info object
117
+ # @param owner [#to_s, Array<#to_s>] the owner(s) of the jobs
118
+ # @param attrs [Array<symbol>] defaults to nil (and all attrs are provided)
119
+ # This array specifies only attrs you want, in addition to id and status.
120
+ # If an array, the Info object that is returned to you is not guarenteed
121
+ # to have a value for any attr besides the ones specified and id and status.
122
+ #
123
+ # For certain adapters this may speed up the response since
124
+ # adapters can get by without populating the entire Info object
125
+ # @yield [Info] of each job to block
126
+ # @return [Enumerator] if no block given
127
+ def info_where_owner_each(owner, attrs: nil)
128
+ return to_enum(:info_where_owner_each, owner, attrs: attrs) unless block_given?
129
+
130
+ info_where_owner(owner, attrs: attrs).each do |job|
131
+ yield job
132
+ end
133
+ end
134
+
135
+ # Whether the adapter supports job arrays
136
+ # @return [Boolean] - assumes true; but can be overridden by adapters that
137
+ # explicitly do not
138
+ def supports_job_arrays?
139
+ false
140
+ end
141
+
142
+ # Retrieve job info from the resource manager
143
+ # @abstract Subclass is expected to implement {#info}
144
+ # @raise [NotImplementedError] if subclass did not define {#info}
145
+ # @param id [#to_s] the id of the job
146
+ # @return [Info] information describing submitted job
147
+ def info(id)
148
+ batch.info(id.to_s)
149
+ rescue Batch::Error => e
150
+ raise JobAdapterError, e.message
151
+ end
152
+
153
+ # Retrieve job status from resource manager
154
+ # @note Optimized slightly over retrieving complete job information from server
155
+ # @abstract Subclass is expected to implement {#status}
156
+ # @raise [NotImplementedError] if subclass did not define {#status}
157
+ # @param id [#to_s] the id of the job
158
+ # @return [Status] status of job
159
+ def status(id)
160
+ info(id).status
161
+ end
162
+
163
+ # Put the submitted job on hold
164
+ # @abstract Subclass is expected to implement {#hold}
165
+ # @raise [NotImplementedError] if subclass did not define {#hold}
166
+ # @param id [#to_s] the id of the job
167
+ # @return [void]
168
+ def hold(id)
169
+ raise NotImplementedError, 'subclass did not define #hold'
170
+ end
171
+
172
+ # Release the job that is on hold
173
+ # @abstract Subclass is expected to implement {#release}
174
+ # @raise [NotImplementedError] if subclass did not define {#release}
175
+ # @param id [#to_s] the id of the job
176
+ # @return [void]
177
+ def release(id)
178
+ raise NotImplementedError, 'subclass did not define #release'
179
+ end
180
+
181
+ # Delete the submitted job.
182
+ #
183
+ # @param id [#to_s] the id of the job
184
+ # @return [void]
185
+ def delete(id)
186
+ batch.delete(id.to_s)
187
+ rescue Batch::Error => e
188
+ raise JobAdapterError, e.message
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -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