resque-kubernetes 0.10.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 15ceb41cf5857885f0b6777411e1f0980a4007bc
4
- data.tar.gz: b1a5f893856a48ac7fdbd0059ebef225e938d474
3
+ metadata.gz: dd653a29a38d097f457584f8bb8a4b305dc14f1f
4
+ data.tar.gz: 4ac673bc617fdd91c99fe3b51f0c943f214655f6
5
5
  SHA512:
6
- metadata.gz: 41c6a804868eb7e64ea25ee33fd08a63ea48756893417837a34440d6c5c01ead8e0eb8467d94888b94ac968047913eda67a0af06133e8aebb1585c80ab69e5e2
7
- data.tar.gz: 1ef4222a4ec9b3508a5237125da66dfc64982a2891530feadbc4d289a5dfe7c747f756fde24bb4f4bdf7e4e4a0a03ed5756c30f3a2b13f83c2ea1207a2766dda
6
+ metadata.gz: 142a38eea17c227b6339eef837b58e4835d3fdf05d7f76f85f30cde00c25f6d20b3ee1fa59795b030549283bfcae28eb49da8159449bd3004066c258c5a7b4d0
7
+ data.tar.gz: 707c7a76de380e87d8da7c4f4651d924f9b7992d5bdedcf48150a0ce8c225cda21d9f252f4a4b907dae373c5c012be2f9ec449bb69d90501a711607f2161b3e2
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
- /.bundle/
1
+ **/.bundle/
2
2
  /.yardoc
3
3
  /Gemfile.lock
4
+ **/*.gemfile.lock
4
5
  /_yardoc/
5
6
  /coverage/
6
7
  /doc/
data/Appraisals ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise "kubeclient-3" do
4
+ gem "kubeclient", "3.1.2"
5
+ end
6
+
7
+ appraise "kubeclient-4" do
8
+ gem "kubeclient", "4.0.0"
9
+ end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ # v1.1.0
2
+ - Fix design to set `INTERVAL=0` for the worker in the Kubernetes
3
+ job manifest, which tells `resque` to work until there are no more jobs,
4
+ rather than monkey-patching `Resque::Worker` to look for `TERM_ON_EMPTY`
5
+ environment variable.
6
+
7
+ # v1.0.0
8
+ **Breaking Change:**
9
+ - Requires `kubeclient` 3.1.2 or 4.x
10
+
11
+ **Changes:**
12
+ - Add `kubeclient` configuration option for connecting to any Kubernetes server
13
+ - Use kubernetes namespace provided by cluster or `kubectl` configuration when available
14
+ - Add Appraisal for testing with kubeclient 3.1.2 and 4.x
15
+
1
16
  # v0.10.0
2
17
  - `kubeclient` may not be later than 3.0.0 due to change in signature of `Kubeclient::Config::Context#initialize`
3
18
  in `kubeclient` 3.1.0
data/README.md CHANGED
@@ -7,8 +7,8 @@ the container finishes and then it terminates the pod (as opposed to trying to
7
7
  restart the container).
8
8
 
9
9
  This gem takes advantage of that feature by starting up a Kubernetes Job to
10
- run a worker when a Resque job or ActiveJob is enqueued. It then allows the
11
- Resque worker to be modified to terminate when there are no more jobs in the queue.
10
+ run a worker when a Resque job or ActiveJob is enqueued. It then tells the
11
+ Resque worker to run until there are no more jobs in the queue.
12
12
 
13
13
  Why would you do this?
14
14
 
@@ -122,15 +122,18 @@ class ResourceIntensiveJob < ApplicationJob
122
122
  end
123
123
  end
124
124
  ```
125
-
126
125
  ### Workers (for both)
127
126
 
128
- Make sure that the container image above, which is used to run the resque
129
- worker, is built to include the `resque-kubernetes` gem as well. The gem will
130
- add `TERM_ON_EMPTY` to the environment variables. This tells the worker that
131
- whenever the queue is empty it should terminate the worker. Kubernetes will
132
- then terminate the Job when the container is done running and will release the
133
- resources.
127
+ The resque worker can can be any container image that runs the `resque:work` `rake` task, for example:
128
+
129
+ ```bash
130
+ bin/rails environment resque:work
131
+ ```
132
+
133
+ The gem sets the environment variable `INTERVAL=0` for the Kubernetes Job which the `rake` task uses
134
+ to when calling `Resque::Worker#work(interval)`. The value 0 tells Resque to terminate when the queue
135
+ is empty. If your Docker image does not run the rake task, then you'll need to make sure you pass 0
136
+ for the interval when calling `Resque::Worker#work`.
134
137
 
135
138
  ### Job manifest
136
139
 
@@ -200,13 +203,28 @@ class ResourceIntensiveJob
200
203
  end
201
204
  ```
202
205
 
203
- ## To Do
206
+ ### kubeclient
207
+
208
+ The gem will automatically connect to the Kubernetes server in the following cases:
209
+ - You are running this in [a standard Kubernetes cluster](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod)
210
+ - You are running on a system with `kubeclient` installed and
211
+ - the default cluster context has credentials
212
+ - the default cluster is GKE and your system has
213
+ [Google application default credentials](https://developers.google.com/identity/protocols/application-default-credentials)
214
+ installed
215
+
216
+ There are many other ways to connect and you can do so by providing your own
217
+ [configured `kubeclient`](https://github.com/abonas/kubeclient#usage):
218
+
219
+ ```ruby
220
+ # config/initializers/resque-kubernetes.rb
221
+
222
+ Resque::Kubernetes.configuration do |config|
223
+ config.kubeclient = Kubeclient::Client.new("http://localhost:8080/apis/batch")
224
+ end
225
+ ```
204
226
 
205
- - Support for other authentication and server URL options for `kubeclient`.
206
- See [the many examples](https://github.com/abonas/kubeclient#usage) in their
207
- README.
208
- - We probably need better namespace support, particularly for reaping
209
- finished jobs and pods.
227
+ Because this uses the `Job` resource, make sure to connect to the `/apis/batch` API endpoint in your client.
210
228
 
211
229
  ## Contributing
212
230
 
@@ -231,7 +249,7 @@ experiment.
231
249
 
232
250
  Write test for any code that you add. Test all changes by running `rake`.
233
251
  This does the following, which you can also run separately while working.
234
- 1. Tun unit tests: `rake spec`
252
+ 1. Run unit tests: `appraisal rake spec`
235
253
  2. Make sure that your code matches the styles: `rubocop`
236
254
  3. Verify if any dependent gems have open CVEs (you must update these):
237
255
  `rake bundle:audit`
@@ -239,10 +257,10 @@ This does the following, which you can also run separately while working.
239
257
  ### End to End Tests
240
258
 
241
259
  We don't run End to End (e2e) tests in the regular suite because
242
- they require a connection to a cluster. You can run these on your changes
243
- if you want to verify that the jobs are created correctly.
260
+ they require a connection to a cluster. You should run these on your changes
261
+ to verify that the jobs are created correctly.
244
262
 
245
- This will use the default authentication on your system, which may is either
263
+ This will use the default authentication on your system, which is either
246
264
  the cluster the tests are running in (if you are doing that), your `kubclient`
247
265
  configuration, or your Google Default Application Credentials.
248
266
 
data/Rakefile CHANGED
@@ -1,16 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "appraisal/task"
4
+ require "bundler/audit/task"
3
5
  require "bundler/gem_tasks"
4
6
  require "rspec/core/rake_task"
5
7
  require "rubocop/rake_task"
6
- require "bundler/audit/task"
7
8
 
8
9
  RSpec::Core::RakeTask.new(:spec)
9
10
  RuboCop::RakeTask.new
10
11
  Bundler::Audit::Task.new
12
+ Appraisal::Task.new
11
13
 
12
14
  # Remove default and replace with a series of test tasks
13
15
  task default: []
14
16
  Rake::Task[:default].clear
15
17
 
16
- task default: %w[spec rubocop bundle:audit]
18
+ if ENV["APPRAISAL_INITIALIZED"]
19
+ task default: %i[spec]
20
+ else
21
+ task default: %i[rubocop bundle:audit appraisal]
22
+ end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "kubeclient", "3.1.2"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "kubeclient", "4.0.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resque
4
+ module Kubernetes
5
+ module Context
6
+ # Kubeclient Context from `kubectl` config file.
7
+ class Kubectl
8
+ def applicable?
9
+ File.exist?(kubeconfig)
10
+ end
11
+
12
+ def context
13
+ config = Kubeclient::Config.read(kubeconfig)
14
+
15
+ Resque::Kubernetes::ContextFactory::Context.new(
16
+ config.context.api_endpoint,
17
+ config.context.api_version,
18
+ config.context.namespace,
19
+ auth_options: auth_options(config),
20
+ ssl_options: config.context.ssl_options
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def kubeconfig
27
+ File.join(ENV["HOME"], ".kube", "config")
28
+ end
29
+
30
+ def auth_options(config)
31
+ options = config.context.auth_options
32
+ return options unless options.empty?
33
+ google_application_default_credentials
34
+ end
35
+
36
+ def google_application_default_credentials
37
+ return unless defined?(Google) && defined?(Google::Auth)
38
+ {bearer_token: Kubeclient::GoogleApplicationDefaultCredentials.token}
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resque
4
+ module Kubernetes
5
+ module Context
6
+ # Kubeclient Context from well-known locations within a Kubernetes cluster.
7
+ class WellKnown
8
+ TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"
9
+ CA_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
10
+ NAMESPACE_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
11
+
12
+ def applicable?
13
+ File.exist?(TOKEN_FILE)
14
+ end
15
+
16
+ def context
17
+ Resque::Kubernetes::ContextFactory::Context.new(
18
+ "https://kubernetes.default.svc",
19
+ "v1",
20
+ namespace,
21
+ auth_options: {bearer_token_file: TOKEN_FILE},
22
+ ssl_options: ssl_options
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def namespace
29
+ return nil unless File.exist?(NAMESPACE_FILE)
30
+ File.read(NAMESPACE_FILE)
31
+ end
32
+
33
+ def ssl_options
34
+ return {} unless File.exist?(CA_FILE)
35
+ {ca_file: CA_FILE}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -6,64 +6,19 @@ module Resque
6
6
  module Kubernetes
7
7
  # Create a context for `Kubeclient` depending on the environment.
8
8
  class ContextFactory
9
+ Context = Struct.new(:endpoint, :version, :namespace, :options)
10
+
9
11
  class << self
10
12
  def context
11
13
  # TODO: Add ability to load this from config
12
-
13
- if File.exist?("/var/run/secrets/kubernetes.io/serviceaccount/token")
14
- # When running in GKE/k8s cluster, use the service account secret token and ca bundle
15
- well_known_context
16
- elsif File.exist?(kubeconfig)
17
- # When running in development, use the config file for `kubectl` and default application credentials
18
- kubectl_context
14
+ [
15
+ Resque::Kubernetes::Context::WellKnown,
16
+ Resque::Kubernetes::Context::Kubectl
17
+ ].each do |context_type|
18
+ context = context_type.new
19
+ return context.context if context.applicable?
19
20
  end
20
21
  end
21
-
22
- private
23
-
24
- def well_known_context
25
- Kubeclient::Config::Context.new(
26
- "https://kubernetes",
27
- "v1",
28
- {ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"},
29
- bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token"
30
- )
31
- end
32
-
33
- def kubectl_context
34
- config = Kubeclient::Config.read(kubeconfig)
35
- auth_options = config.context.auth_options
36
-
37
- auth_options = google_default_application_credentials(config) if auth_options.empty?
38
-
39
- Kubeclient::Config::Context.new(
40
- config.context.api_endpoint,
41
- config.context.api_version,
42
- config.context.ssl_options,
43
- auth_options
44
- )
45
- end
46
-
47
- def kubeconfig
48
- File.join(ENV["HOME"], ".kube", "config")
49
- end
50
-
51
- # TODO: Move this logic to kubeclient. See abonas/kubeclient#213
52
- def google_default_application_credentials(config)
53
- return unless defined?(Google) && defined?(Google::Auth)
54
-
55
- _cluster, user = config.send(:fetch_context, config.instance_variable_get(:@kcfg)["current-context"])
56
- return {} unless user["auth-provider"] && user["auth-provider"]["name"] == "gcp"
57
-
58
- {bearer_token: new_google_token}
59
- end
60
-
61
- def new_google_token
62
- scopes = ["https://www.googleapis.com/auth/cloud-platform"]
63
- authorization = Google::Auth.get_application_default(scopes)
64
- authorization.apply({})
65
- authorization.access_token
66
- end
67
22
  end
68
23
  end
69
24
  end
@@ -10,7 +10,8 @@ module Resque
10
10
  private :owner
11
11
 
12
12
  def initialize(owner)
13
- @owner = owner
13
+ @owner = owner
14
+ @default_namespace = "default"
14
15
  end
15
16
 
16
17
  def reap_finished_jobs
@@ -39,20 +40,17 @@ module Resque
39
40
  private
40
41
 
41
42
  def jobs_client
42
- return @jobs_client if @jobs_client
43
- @jobs_client = client("/apis/batch")
43
+ @jobs_client ||= client("/apis/batch")
44
44
  end
45
45
 
46
46
  def client(scope)
47
+ return Resque::Kubernetes.kubeclient if Resque::Kubernetes.kubeclient
48
+
47
49
  context = ContextFactory.context
48
50
  return unless context
51
+ @default_namespace = context.namespace if context.namespace
49
52
 
50
- Kubeclient::Client.new(
51
- context.api_endpoint + scope,
52
- context.api_version,
53
- ssl_options: context.ssl_options,
54
- auth_options: context.auth_options
55
- )
53
+ Kubeclient::Client.new(context.endpoint + scope, context.version, context.options)
56
54
  end
57
55
 
58
56
  def finished_jobs
@@ -92,12 +90,12 @@ module Resque
92
90
 
93
91
  def container_term_on_empty(container)
94
92
  container["env"] ||= []
95
- term_on_empty = container["env"].find { |env| env["name"] == "TERM_ON_EMPTY" }
93
+ term_on_empty = container["env"].find { |env| env["name"] == "INTERVAL" }
96
94
  unless term_on_empty
97
- term_on_empty = {"name" => "TERM_ON_EMPTY"}
95
+ term_on_empty = {"name" => "INTERVAL"}
98
96
  container["env"] << term_on_empty
99
97
  end
100
- term_on_empty["value"] = "1"
98
+ term_on_empty["value"] = "0"
101
99
  end
102
100
 
103
101
  def ensure_reset_policy(manifest)
@@ -105,7 +103,7 @@ module Resque
105
103
  end
106
104
 
107
105
  def ensure_namespace(manifest)
108
- manifest["metadata"]["namespace"] ||= "default"
106
+ manifest["metadata"]["namespace"] ||= @default_namespace
109
107
  end
110
108
 
111
109
  def update_job_name(manifest)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Resque
4
4
  module Kubernetes
5
- VERSION = "0.10.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "resque/kubernetes/configurable"
4
+ require "resque/kubernetes/context/kubectl"
5
+ require "resque/kubernetes/context/well_known"
4
6
  require "resque/kubernetes/context_factory"
5
7
  require "resque/kubernetes/deep_hash"
6
8
  require "resque/kubernetes/dns_safe_random"
7
9
  require "resque/kubernetes/job"
8
10
  require "resque/kubernetes/jobs_manager"
9
11
  require "resque/kubernetes/version"
10
- require "resque/kubernetes/worker"
11
12
 
12
13
  module Resque
13
14
  # Run Resque Jobs as Kubernetes Jobs with autoscaling.
@@ -19,7 +20,8 @@ module Resque
19
20
 
20
21
  # Limit the number of workers that should be spun up, default 10
21
22
  define_setting :max_workers, 10
23
+
24
+ # A `kubeclient` for connection context, default attempts to read from cluster or `~/.kube/config`
25
+ define_setting :kubeclient, nil
22
26
  end
23
27
  end
24
-
25
- Resque::Worker.include Resque::Kubernetes::Worker
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ["lib"]
25
25
 
26
+ spec.add_development_dependency "appraisal"
26
27
  spec.add_development_dependency "bundler", "~> 1.16"
27
28
  spec.add_development_dependency "bundler-audit", "~> 0"
28
29
  spec.add_development_dependency "googleauth", "~> 0.6"
@@ -30,6 +31,6 @@ Gem::Specification.new do |spec|
30
31
  spec.add_development_dependency "rspec", "~> 3.7"
31
32
  spec.add_development_dependency "rubocop", "~> 0.52", ">= 0.52.1"
32
33
 
33
- spec.add_dependency "kubeclient", ">= 2.2", "<= 3.0.0"
34
+ spec.add_dependency "kubeclient", ">= 3.1.2", "< 5.0"
34
35
  spec.add_dependency "resque", "~> 1.26"
35
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-kubernetes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Wadsack
@@ -10,6 +10,20 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2018-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: appraisal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -106,20 +120,20 @@ dependencies:
106
120
  requirements:
107
121
  - - ">="
108
122
  - !ruby/object:Gem::Version
109
- version: '2.2'
110
- - - "<="
123
+ version: 3.1.2
124
+ - - "<"
111
125
  - !ruby/object:Gem::Version
112
- version: 3.0.0
126
+ version: '5.0'
113
127
  type: :runtime
114
128
  prerelease: false
115
129
  version_requirements: !ruby/object:Gem::Requirement
116
130
  requirements:
117
131
  - - ">="
118
132
  - !ruby/object:Gem::Version
119
- version: '2.2'
120
- - - "<="
133
+ version: 3.1.2
134
+ - - "<"
121
135
  - !ruby/object:Gem::Version
122
- version: 3.0.0
136
+ version: '5.0'
123
137
  - !ruby/object:Gem::Dependency
124
138
  name: resque
125
139
  requirement: !ruby/object:Gem::Requirement
@@ -147,6 +161,7 @@ files:
147
161
  - ".rubocop.yml"
148
162
  - ".ruby-gemset"
149
163
  - ".ruby-version"
164
+ - Appraisals
150
165
  - CHANGELOG.md
151
166
  - Gemfile
152
167
  - LICENSE.txt
@@ -154,15 +169,18 @@ files:
154
169
  - Rakefile
155
170
  - bin/console
156
171
  - bin/setup
172
+ - gemfiles/kubeclient_3.gemfile
173
+ - gemfiles/kubeclient_4.gemfile
157
174
  - lib/resque/kubernetes.rb
158
175
  - lib/resque/kubernetes/configurable.rb
176
+ - lib/resque/kubernetes/context/kubectl.rb
177
+ - lib/resque/kubernetes/context/well_known.rb
159
178
  - lib/resque/kubernetes/context_factory.rb
160
179
  - lib/resque/kubernetes/deep_hash.rb
161
180
  - lib/resque/kubernetes/dns_safe_random.rb
162
181
  - lib/resque/kubernetes/job.rb
163
182
  - lib/resque/kubernetes/jobs_manager.rb
164
183
  - lib/resque/kubernetes/version.rb
165
- - lib/resque/kubernetes/worker.rb
166
184
  - resque-kubernetes.gemspec
167
185
  homepage: https://github.com/keylimetoolbox/resque-kubernetes
168
186
  licenses:
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "resque"
4
-
5
- module Resque
6
- module Kubernetes
7
- # Patches the resque worker to terminate when the queue is empty.
8
- #
9
- # This patch enables setting an environment variable, `TERM_ON_EMPTY`
10
- # that causes the worker to shutdown when the queue no longer has any
11
- # resque jobs. This allows running workers as Kuberenetes Jobs that will
12
- # terminate when there is no longer any work to do.
13
- #
14
- # To use, make sure that the container images that hold your workers are
15
- # built to include the `resque-kubernetes` gem and set `TERM_ON_EMPTY` in
16
- # their environment to a truthy value (e.g. "1").
17
- module Worker
18
- def self.included(base)
19
- base.class_eval do
20
- prepend InstanceMethods
21
- end
22
- end
23
-
24
- attr_accessor :term_on_empty
25
-
26
- # Replace methods on the worker instance
27
- module InstanceMethods
28
- def prepare
29
- self.term_on_empty = ENV["TERM_ON_EMPTY"] if ENV["TERM_ON_EMPTY"]
30
- super
31
- end
32
-
33
- def shutdown?
34
- if term_on_empty
35
- if queues_empty?
36
- log_with_severity :info, "shutdown: queues are empty"
37
- shutdown
38
- end
39
- end
40
-
41
- super
42
- end
43
- end
44
-
45
- private
46
-
47
- def queues_empty?
48
- queues.all? { |queue| Resque.size(queue).zero? }
49
- end
50
- end
51
- end
52
- end