resque-kubernetes 0.10.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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