cron-kubernetes 0.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 +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +42 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/cron_kubernetes +29 -0
- data/bin/setup +8 -0
- data/cron-kubernetes.gemspec +38 -0
- data/lib/cron-kubernetes.rb +1 -0
- data/lib/cron_kubernetes.rb +34 -0
- data/lib/cron_kubernetes/configurable.rb +34 -0
- data/lib/cron_kubernetes/cron_job.rb +76 -0
- data/lib/cron_kubernetes/cron_tab.rb +73 -0
- data/lib/cron_kubernetes/kubeclient_context.rb +68 -0
- data/lib/cron_kubernetes/kubernetes_client.rb +24 -0
- data/lib/cron_kubernetes/scheduler.rb +64 -0
- data/lib/cron_kubernetes/version.rb +5 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f95641f4e595e197c2205b213097349df8b5f93c4f4c6556416f66bb6c1dd636
|
4
|
+
data.tar.gz: 3fb3993388b2f631c1b73e297a18340b45c197368a24486b7b5f3ca7cb7395bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 128dc9682185ff67c49804d099a05056f6403ef4f25ad6161401d8fbf10881b325e141b2dd631445db2570e67c8ba499fb05eef433372e1feedc08a813c4f03e
|
7
|
+
data.tar.gz: 267f422c41cceee4a2bef3293918504322c6f468573eb4158898c71027b3d6ee28973178021af25cc951f9b7d26606c12c9fc77e65a132ca442a6ca6bb506584
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Documentation:
|
2
|
+
Exclude:
|
3
|
+
- "**/railtie.rb"
|
4
|
+
- "spec/**/*"
|
5
|
+
|
6
|
+
Style/StringLiterals:
|
7
|
+
EnforcedStyle: double_quotes
|
8
|
+
Metrics/LineLength:
|
9
|
+
Max: 120
|
10
|
+
Layout/AlignHash:
|
11
|
+
EnforcedHashRocketStyle: table
|
12
|
+
EnforcedColonStyle: table
|
13
|
+
Layout/SpaceInsideHashLiteralBraces:
|
14
|
+
EnforcedStyle: no_space
|
15
|
+
Style/RaiseArgs:
|
16
|
+
EnforcedStyle: compact
|
17
|
+
Style/EmptyMethod:
|
18
|
+
EnforcedStyle: expanded
|
19
|
+
Layout/IndentArray:
|
20
|
+
IndentationWidth: 4
|
21
|
+
Layout/IndentHash:
|
22
|
+
IndentationWidth: 4
|
23
|
+
Style/ConditionalAssignment:
|
24
|
+
EnforcedStyle: assign_inside_condition
|
25
|
+
Layout/FirstParameterIndentation:
|
26
|
+
IndentationWidth: 4
|
27
|
+
Layout/MultilineOperationIndentation:
|
28
|
+
IndentationWidth: 4
|
29
|
+
EnforcedStyle: indented
|
30
|
+
Style/FormatStringToken:
|
31
|
+
EnforcedStyle: template
|
32
|
+
Style/AsciiComments:
|
33
|
+
Enabled: false
|
34
|
+
|
35
|
+
Metrics/BlockLength:
|
36
|
+
Exclude:
|
37
|
+
- "*.gemspec"
|
38
|
+
- "spec/**/*"
|
39
|
+
|
40
|
+
Layout/EmptyLinesAroundBlockBody:
|
41
|
+
Exclude:
|
42
|
+
- "spec/**/*"
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
cron-kubernetes-ruby
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.0
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Jeremy Wadsack
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# CronKubernetes
|
2
|
+
|
3
|
+
Configue and deploy Kubernetes [CronJobs](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/)
|
4
|
+
from ruby.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "cron-kubernetes"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install cron_kubernetes
|
21
|
+
|
22
|
+
## Configuration
|
23
|
+
|
24
|
+
You can configure global settings for your cron jobs. Add a file to your source like the example
|
25
|
+
below. If you are using Rails, you can add this to something like `config/initializers/cron_kuberentes.rb`.
|
26
|
+
|
27
|
+
You _must_ configure the `identifier` and `manifest` settings. The other settings are optional
|
28
|
+
and default values are shown below.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
CronKubernetes.configuration do |config|
|
32
|
+
# Required
|
33
|
+
config.identifier = "my-application"
|
34
|
+
config.manifest = YAML.load_file(File.join(Rails.root, "deploy", "kubernetes-job.yml"))
|
35
|
+
|
36
|
+
# Optional
|
37
|
+
config.output = nil
|
38
|
+
config.job_template = %w[/bin/bash -l -c :job]
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
### `identifier`
|
43
|
+
Provide an identifier for this schedule. For example, you might use your application name.
|
44
|
+
This is used by `CronKubernetes` to know which CronJobs are associated with this schedule
|
45
|
+
so you should make sure it's unique within your cluster.
|
46
|
+
|
47
|
+
`identifier` must be valid for a Kubernetes resource name and label value. Specifically,
|
48
|
+
lowercase alphanumeric characters (`[a-z0-9A-Z]`), `-`, and `.`, and 63 characters or less.
|
49
|
+
|
50
|
+
### `manifest`
|
51
|
+
|
52
|
+
This is a Kubernetes Job manifest used as the job template within the Kubernetes
|
53
|
+
CronJob. That is, this is the job that's started at the specified schedule. For
|
54
|
+
example:
|
55
|
+
|
56
|
+
```yaml
|
57
|
+
apiVersion: batch/v1
|
58
|
+
kind: Job
|
59
|
+
metadata:
|
60
|
+
name: scheduled-job
|
61
|
+
spec:
|
62
|
+
template:
|
63
|
+
metadata:
|
64
|
+
name: scheduled-job
|
65
|
+
spec:
|
66
|
+
containers:
|
67
|
+
- name: my-shell
|
68
|
+
image: ubuntu
|
69
|
+
restartPolicy: OnFailure
|
70
|
+
```
|
71
|
+
|
72
|
+
In the example above we show the manifest loading a file, just to make it
|
73
|
+
simple. But you could also read use a HEREDOC, parse a template and insert
|
74
|
+
values, or anything else you want to do in the method, as long as you return
|
75
|
+
a valid Kubernetes Job manifest as a `Hash`.
|
76
|
+
|
77
|
+
When the job is run, the default command in the Docker instance is replaced with
|
78
|
+
the command specified in the cron schedule (see below). The command is run on the
|
79
|
+
first container in the pod.
|
80
|
+
|
81
|
+
### `output`
|
82
|
+
|
83
|
+
By default no redirection is done; cron behaves as normal. If you would like you
|
84
|
+
can specify an option here to redirect as you would on a shell command. For example,
|
85
|
+
`"2>&1` to collect STDERR in STDOUT or `>> /var/log/task.log` to append to a log file.
|
86
|
+
|
87
|
+
### `job_template`
|
88
|
+
|
89
|
+
This is a template that we use to execute your rake, rails runner, or shell command
|
90
|
+
in the container. The default template executes it in a login shell so that environment
|
91
|
+
variables (and profile) are loaded.
|
92
|
+
|
93
|
+
You can modify this. The value should be an array with a command and arguments that will
|
94
|
+
replace both `ENTRYPOINT` and `CMD` in the Docker image. See
|
95
|
+
[Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/)
|
96
|
+
for a discussion of how `command` works in Kubernetes.
|
97
|
+
|
98
|
+
## Usage
|
99
|
+
|
100
|
+
### Create a Schedule
|
101
|
+
Add a file to your source that defines the scheduled tasks. If you are using Rails, you could
|
102
|
+
put this in `config/initializers/cron_kuberentes.rb`. Or, if you want to make it work like the
|
103
|
+
`whenever` gem you could add these lines to `config/schedule.rb` and then `require` that from your
|
104
|
+
initializer.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
CronKubernetes.schedule do
|
108
|
+
command "ls -l", schedule: "0 0 1 1 *"
|
109
|
+
rake "audit:state", schedule: "0 20 1 * *", name: "audit-state"
|
110
|
+
runner "CleanSweeper.run", schedule: "30 3 * * *"
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
For all jobs the command with change directories to either `Rails.root` if Rails is installed
|
115
|
+
or the current working directory. These are evaluated when the scheduled tasks are loaded.
|
116
|
+
|
117
|
+
For all jobs you may provide a `name` to name, which will be used with the `identifier` to name the
|
118
|
+
CronJob. If you do not provide a name `CronKubernetes` will try to figure one out from the job and
|
119
|
+
pod templates plus a hash of the schedule and command.
|
120
|
+
|
121
|
+
#### Shell Commands
|
122
|
+
|
123
|
+
A `command` runs any arbitrary shell command on a schedule. The first argument is the command to run.
|
124
|
+
|
125
|
+
|
126
|
+
#### Rake Tasks
|
127
|
+
|
128
|
+
A `rake` call runs a `rake` task on the schedule. Rake and Bundler must be installed and on the path
|
129
|
+
in the container The command it executes is `bundle exec rake ...`.
|
130
|
+
|
131
|
+
#### Runners
|
132
|
+
|
133
|
+
A `runner` runs arbitrary ruby code under rails. Rails must be installed at `bin/rails` from the
|
134
|
+
working folder. The command it executes is `bin/rails runner '...'`.
|
135
|
+
|
136
|
+
### Update Your Cluster
|
137
|
+
|
138
|
+
Once you have configuration and cluster, then you can run the `cron_kubernetes` command
|
139
|
+
to update your cluster.
|
140
|
+
|
141
|
+
```bash
|
142
|
+
cron_kubernetes --configuration config/initializers/cron_kubernetes.rb --schedule config/schedule.rb
|
143
|
+
```
|
144
|
+
|
145
|
+
The command will read the provided configuration and current schedule, compare to any
|
146
|
+
CronJobs already in your cluster for this project (base on the `identifier`) and then
|
147
|
+
add/remove/update the CronJobs to bring match the schedule.
|
148
|
+
|
149
|
+
You can provide either `--configuration` or `--schedule`, as long as between the files you have
|
150
|
+
loaded both a configuration and a schedule. For example, if they are in the same file, you would
|
151
|
+
just pass a single value:
|
152
|
+
|
153
|
+
```bash
|
154
|
+
cron_kubernetes --schedule schedule.rb
|
155
|
+
```
|
156
|
+
|
157
|
+
If you are running in a Rails application where the initializers are auto-loaded, and your
|
158
|
+
schedule is defined in (or in a file required by) your initializer, you could run this within
|
159
|
+
your Rails environment:
|
160
|
+
|
161
|
+
```bash
|
162
|
+
bin/rails runner cron_kubernetes
|
163
|
+
```
|
164
|
+
|
165
|
+
## To Do
|
166
|
+
- In place of `schedule`, support `every`/`at` syntax:
|
167
|
+
```
|
168
|
+
every: :minute, :hour, :day, :month, :year
|
169
|
+
3.minutes, 1.hour, 1.day, 1.week, 1.month, 1.year
|
170
|
+
at: "[H]H:mm[am|pm]"
|
171
|
+
```
|
172
|
+
|
173
|
+
## Development
|
174
|
+
|
175
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
176
|
+
|
177
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
178
|
+
|
179
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
180
|
+
|
181
|
+
To release a new version, update the version number in `lib/cron_kubernets/version.rb` and the `CHANGELOG.md`,
|
182
|
+
and then run `bundle exec rake release`, which will create a git tag for the version,
|
183
|
+
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
184
|
+
|
185
|
+
## Contributing
|
186
|
+
|
187
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/keylimetoolbox/cron-kubernetes.
|
188
|
+
|
189
|
+
## Acknowledgments
|
190
|
+
|
191
|
+
We have used the [`whenever` gem](https://github.com/javan/whenever) for years and we love it.
|
192
|
+
Much of the ideas for scheduling here were inspired by the great work that @javan and team
|
193
|
+
have put into that gem.
|
194
|
+
|
195
|
+
## License
|
196
|
+
|
197
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "rubocop/rake_task"
|
6
|
+
require "bundler/audit/task"
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
RuboCop::RakeTask.new
|
10
|
+
Bundler::Audit::Task.new
|
11
|
+
|
12
|
+
# Remove default and replace with a series of test tasks
|
13
|
+
task default: []
|
14
|
+
Rake::Task[:default].clear
|
15
|
+
|
16
|
+
task default: %w[spec rubocop bundle:audit]
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "cron_kubernetes"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/cron_kubernetes
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "cron_kubernetes"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
# Support looking up Google Default Application Credentials, if the gem is installed
|
8
|
+
begin
|
9
|
+
require "googleauth"
|
10
|
+
rescue LoadError
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: cron_kubernetes [options]"
|
16
|
+
opts.on("-c", "--configuration [file]", "Location of your configuration file") do |file|
|
17
|
+
require File.join(Dir.pwd, file) if file
|
18
|
+
end
|
19
|
+
opts.on("-c", "--schedule [file]", "Location of your schedule file") do |file|
|
20
|
+
require File.join(Dir.pwd, file) if file
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-v", "--version") do
|
24
|
+
puts "CronKubernetes v#{CronKubernetes::VERSION}"
|
25
|
+
exit(0)
|
26
|
+
end
|
27
|
+
end.parse!
|
28
|
+
|
29
|
+
CronKubernetes::CronTab.new.update
|
data/bin/setup
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path("lib", __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require "cron_kubernetes/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "cron-kubernetes"
|
10
|
+
spec.version = CronKubernetes::VERSION
|
11
|
+
spec.authors = ["Jeremy Wadsack"]
|
12
|
+
spec.email = ["jeremy.wadsack@gmail.com"]
|
13
|
+
|
14
|
+
spec.summary = "Configure and deploy Kubernetes CronJobs from ruby."
|
15
|
+
spec.description = "Configure and deploy Kubernetes CronJobs from ruby with a single schedule."
|
16
|
+
spec.homepage = "https://github.com/keylimetoolbox/cron-kubernetes"
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.bindir = "bin"
|
25
|
+
spec.executables << "cron_kubernetes"
|
26
|
+
|
27
|
+
spec.add_dependency "kubeclient", "~> 3.0"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
30
|
+
spec.add_development_dependency "bundler-audit", "~> 0"
|
31
|
+
spec.add_development_dependency "mocha", "~> 1.3"
|
32
|
+
spec.add_development_dependency "rake", "~> 12.3"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.7"
|
34
|
+
spec.add_development_dependency "rubocop", "~> 0.52", ">= 0.52.1"
|
35
|
+
|
36
|
+
# For connecting to a GKE cluster in development/test
|
37
|
+
spec.add_development_dependency "googleauth"
|
38
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "cron_kubernetes"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cron_kubernetes/configurable"
|
4
|
+
require "cron_kubernetes/cron_job"
|
5
|
+
require "cron_kubernetes/cron_tab"
|
6
|
+
require "cron_kubernetes/kubeclient_context"
|
7
|
+
require "cron_kubernetes/kubernetes_client"
|
8
|
+
require "cron_kubernetes/scheduler"
|
9
|
+
require "cron_kubernetes/version"
|
10
|
+
|
11
|
+
# Configure and deploy Kubernetes CronJobs from ruby
|
12
|
+
module CronKubernetes
|
13
|
+
extend Configurable
|
14
|
+
|
15
|
+
# Provide a CronJob manifest as a Hash
|
16
|
+
define_setting :manifest
|
17
|
+
|
18
|
+
# Provide shell output redirection (e.g. "2>&1" or ">> log")
|
19
|
+
define_setting :output
|
20
|
+
|
21
|
+
# For RVM support, and to load PATH and such, jobs are run through a bash shell.
|
22
|
+
# You can alter this with your own template, add `:job` where the job should go.
|
23
|
+
# Note that the job will be treated as a single shell argument or command.
|
24
|
+
define_setting :job_template, %w[/bin/bash -l -c :job]
|
25
|
+
|
26
|
+
# Provide an identifier for this schedule (e.g. your application name)
|
27
|
+
define_setting :identifier
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def schedule(&block)
|
31
|
+
CronKubernetes::Scheduler.instance.instance_eval(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CronKubernetes
|
4
|
+
# Provides configuration settings, with default values, for the gem.
|
5
|
+
module Configurable
|
6
|
+
def configuration
|
7
|
+
yield self
|
8
|
+
end
|
9
|
+
|
10
|
+
# Define a configuration setting and its default value.
|
11
|
+
#
|
12
|
+
# name: The name of the setting.
|
13
|
+
# default: A default value for the setting. (Optional)
|
14
|
+
def define_setting(name, default = nil)
|
15
|
+
class_variable_set("@@#{name}", default)
|
16
|
+
|
17
|
+
define_class_method "#{name}=" do |value|
|
18
|
+
class_variable_set("@@#{name}", value)
|
19
|
+
end
|
20
|
+
|
21
|
+
define_class_method name do
|
22
|
+
class_variable_get("@@#{name}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def define_class_method(name, &block)
|
29
|
+
(class << self; self; end).instance_eval do
|
30
|
+
define_method(name, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
|
5
|
+
module CronKubernetes
|
6
|
+
# A single job to run on a given schedule.
|
7
|
+
class CronJob
|
8
|
+
attr_accessor :schedule, :command, :job_manifest, :name, :identifier
|
9
|
+
|
10
|
+
def initialize(schedule: nil, command: nil, job_manifest: nil, name: nil, identifier: nil)
|
11
|
+
@schedule = schedule
|
12
|
+
@command = command
|
13
|
+
@job_manifest = job_manifest
|
14
|
+
@name = name
|
15
|
+
@identifier = identifier
|
16
|
+
end
|
17
|
+
|
18
|
+
# rubocop:disable Metrics/MethodLength
|
19
|
+
def cron_job_manifest
|
20
|
+
{
|
21
|
+
"apiVersion" => "batch/v1beta1",
|
22
|
+
"kind" => "CronJob",
|
23
|
+
"metadata" => {
|
24
|
+
"name" => "#{identifier}-#{cron_job_name}",
|
25
|
+
"namespace" => namespace,
|
26
|
+
"labels" => {"cron-kubernetes-identifier" => identifier}
|
27
|
+
},
|
28
|
+
"spec" => {
|
29
|
+
"schedule" => schedule,
|
30
|
+
"jobTemplate" => {
|
31
|
+
"metadata" => job_metadata,
|
32
|
+
"spec" => job_spec
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
# rubocop:enable Metrics/MethodLength
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def namespace
|
42
|
+
return job_manifest["metadata"]["namespace"] if job_manifest["metadata"]
|
43
|
+
"default"
|
44
|
+
end
|
45
|
+
|
46
|
+
def job_spec
|
47
|
+
spec = job_manifest["spec"].dup
|
48
|
+
first_container = spec["template"]["spec"]["containers"][0]
|
49
|
+
first_container["command"] = command
|
50
|
+
spec
|
51
|
+
end
|
52
|
+
|
53
|
+
def job_metadata
|
54
|
+
job_manifest["metadata"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def cron_job_name
|
58
|
+
return name if name
|
59
|
+
return job_hash(job_manifest["metadata"]["name"]) if job_manifest["metadata"]
|
60
|
+
pod_template_name
|
61
|
+
end
|
62
|
+
|
63
|
+
# rubocop:disable Metrics/AbcSize
|
64
|
+
def pod_template_name
|
65
|
+
return nil unless job_manifest["spec"] &&
|
66
|
+
job_manifest["spec"]["template"] &&
|
67
|
+
job_manifest["spec"]["template"]["metadata"]
|
68
|
+
job_hash(job_manifest["spec"]["template"]["metadata"]["name"])
|
69
|
+
end
|
70
|
+
# rubocop:enable Metrics/AbcSize
|
71
|
+
|
72
|
+
def job_hash(name)
|
73
|
+
"#{name}-#{Digest::SHA1.hexdigest(schedule + command.join)[0..7]}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CronKubernetes
|
4
|
+
# The "table" of Kubernetes CronJobs that we manage in the cluster.
|
5
|
+
class CronTab
|
6
|
+
attr_reader :client
|
7
|
+
private :client
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@client = CronKubernetes::KubernetesClient.new.batch_beta1_client
|
11
|
+
end
|
12
|
+
|
13
|
+
# "Apply" the new configuration
|
14
|
+
# - remove from cluster any cron_jobs that are no longer in the schedule
|
15
|
+
# - add new jobs
|
16
|
+
# - update cron_jobs that exist (deleting a cron_job deletes the job and pod)
|
17
|
+
def update(schedule = nil)
|
18
|
+
schedule ||= CronKubernetes::Scheduler.instance.schedule
|
19
|
+
add, change, remove = diff_schedules(schedule, current_cron_jobs)
|
20
|
+
remove.each { |job| remove_cron_job(job) }
|
21
|
+
add.each { |job| add_cron_job(job) }
|
22
|
+
change.each { |job| update_cron_job(job) }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Define a label for our jobs based on an identifier
|
28
|
+
def label_selector
|
29
|
+
{"cron-kubernetes-identifier" => CronKubernetes.identifier}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Find all k8s CronJobs by our label for the identifier
|
33
|
+
def current_cron_jobs
|
34
|
+
client.get_cron_jobs(label_selector)
|
35
|
+
end
|
36
|
+
|
37
|
+
def diff_schedules(new, existing)
|
38
|
+
new_index = index_cron_jobs(new)
|
39
|
+
existing_index = index_kubernetes_cron_jobs(existing)
|
40
|
+
add_keys = new_index.keys - existing_index.keys
|
41
|
+
remove_keys = existing_index.keys - new_index.keys
|
42
|
+
change_keys = new_index.keys & existing_index.keys
|
43
|
+
|
44
|
+
[
|
45
|
+
new_index.values_at(*add_keys),
|
46
|
+
new_index.values_at(*change_keys),
|
47
|
+
existing_index.values_at(*remove_keys)
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove a Kubeclient::Resource::CronJob from the Kubernetes cluster
|
52
|
+
def remove_cron_job(job)
|
53
|
+
client.delete_cron_job(job.metadata.name, job.metadata.namespace)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Add a CronKubernetes::CronJob to the Kubernetes cluster
|
57
|
+
def add_cron_job(job)
|
58
|
+
client.create_cron_job(Kubeclient::Resource.new(job.cron_job_manifest))
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_cron_job(job)
|
62
|
+
client.update_cron_job(Kubeclient::Resource.new(job.cron_job_manifest))
|
63
|
+
end
|
64
|
+
|
65
|
+
def index_cron_jobs(jobs)
|
66
|
+
jobs.map { |job| ["#{job.identifier}-#{job.name}", job] }.to_h
|
67
|
+
end
|
68
|
+
|
69
|
+
def index_kubernetes_cron_jobs(jobs)
|
70
|
+
jobs.map { |job| [job.metadata.name, job] }.to_h
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "kubeclient"
|
4
|
+
|
5
|
+
module CronKubernetes
|
6
|
+
# Create a context for `Kubeclient` depending on the environment.
|
7
|
+
class KubeclientContext
|
8
|
+
class << self
|
9
|
+
def context
|
10
|
+
# TODO: Add ability to load this from config
|
11
|
+
|
12
|
+
if File.exist?("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
13
|
+
# When running in k8s cluster, use the service account secret token and ca bundle
|
14
|
+
well_known_context
|
15
|
+
elsif File.exist?(kubeconfig)
|
16
|
+
# When running in development, use the config file for `kubectl` and default application credentials
|
17
|
+
kubectl_context
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def well_known_context
|
24
|
+
Kubeclient::Config::Context.new(
|
25
|
+
"https://kubernetes",
|
26
|
+
"v1",
|
27
|
+
{ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"},
|
28
|
+
bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def kubectl_context
|
33
|
+
config = Kubeclient::Config.read(kubeconfig)
|
34
|
+
auth_options = config.context.auth_options
|
35
|
+
|
36
|
+
auth_options = google_default_application_credentials(config) if auth_options.empty?
|
37
|
+
|
38
|
+
Kubeclient::Config::Context.new(
|
39
|
+
config.context.api_endpoint,
|
40
|
+
config.context.api_version,
|
41
|
+
config.context.ssl_options,
|
42
|
+
auth_options
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def kubeconfig
|
47
|
+
File.join(ENV["HOME"], ".kube", "config")
|
48
|
+
end
|
49
|
+
|
50
|
+
# TODO: Move this logic to kubeclient. See abonas/kubeclient#213
|
51
|
+
def google_default_application_credentials(config)
|
52
|
+
return unless defined?(Google) && defined?(Google::Auth)
|
53
|
+
|
54
|
+
_cluster, user = config.send(:fetch_context, config.instance_variable_get(:@kcfg)["current-context"])
|
55
|
+
return {} unless user["auth-provider"] && user["auth-provider"]["name"] == "gcp"
|
56
|
+
|
57
|
+
{bearer_token: new_google_token}
|
58
|
+
end
|
59
|
+
|
60
|
+
def new_google_token
|
61
|
+
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
|
62
|
+
authorization = Google::Auth.get_application_default(scopes)
|
63
|
+
authorization.apply({})
|
64
|
+
authorization.access_token
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CronKubernetes
|
4
|
+
# Encapsulate access to Kubernetes API for different API versions.
|
5
|
+
class KubernetesClient
|
6
|
+
def batch_beta1_client
|
7
|
+
@batch_beta1_client ||= client("/apis/batch", "v1beta1")
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def client(scope, version = nil)
|
13
|
+
context = KubeclientContext.context
|
14
|
+
return unless context
|
15
|
+
|
16
|
+
Kubeclient::Client.new(
|
17
|
+
context.api_endpoint + scope,
|
18
|
+
version || context.api_version,
|
19
|
+
ssl_options: context.ssl_options,
|
20
|
+
auth_options: context.auth_options
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module CronKubernetes
|
6
|
+
# A singleton that creates and holds the scheduled commands.
|
7
|
+
class Scheduler
|
8
|
+
include Singleton
|
9
|
+
attr_reader :schedule
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@schedule = []
|
13
|
+
@identifier = CronKubernetes.identifier
|
14
|
+
end
|
15
|
+
|
16
|
+
def rake(task, schedule:, name: nil)
|
17
|
+
rake_command = "bundle exec rake #{task} --silent"
|
18
|
+
rake_command = "RAILS_ENV=#{rails_env} #{rake_command}" if rails_env
|
19
|
+
@schedule << new_cron_job(schedule, rake_command, name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def runner(ruby_command, schedule:, name: nil)
|
23
|
+
env = nil
|
24
|
+
env = "-e #{rails_env} " if rails_env
|
25
|
+
runner_command = "bin/rails runner #{env}'#{ruby_command}'"
|
26
|
+
@schedule << new_cron_job(schedule, runner_command, name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def command(command, schedule:, name: nil)
|
30
|
+
@schedule << new_cron_job(schedule, command, name)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def make_command(command)
|
36
|
+
CronKubernetes.job_template.map do |arg|
|
37
|
+
if arg == ":job"
|
38
|
+
"cd #{root} && #{command} #{CronKubernetes.output}"
|
39
|
+
else
|
40
|
+
arg
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def new_cron_job(schedule, command, name)
|
46
|
+
CronJob.new(
|
47
|
+
schedule: schedule,
|
48
|
+
command: make_command(command),
|
49
|
+
job_manifest: CronKubernetes.manifest,
|
50
|
+
name: name,
|
51
|
+
identifier: @identifier
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def rails_env
|
56
|
+
ENV["RAILS_ENV"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def root
|
60
|
+
return Rails.root if defined? Rails
|
61
|
+
Dir.pwd
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cron-kubernetes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Wadsack
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kubeclient
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.16'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler-audit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '12.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '12.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.7'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.7'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.52'
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 0.52.1
|
107
|
+
type: :development
|
108
|
+
prerelease: false
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - "~>"
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0.52'
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 0.52.1
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: googleauth
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
description: Configure and deploy Kubernetes CronJobs from ruby with a single schedule.
|
132
|
+
email:
|
133
|
+
- jeremy.wadsack@gmail.com
|
134
|
+
executables:
|
135
|
+
- cron_kubernetes
|
136
|
+
extensions: []
|
137
|
+
extra_rdoc_files: []
|
138
|
+
files:
|
139
|
+
- ".gitignore"
|
140
|
+
- ".rspec"
|
141
|
+
- ".rubocop.yml"
|
142
|
+
- ".ruby-gemset"
|
143
|
+
- ".ruby-version"
|
144
|
+
- CHANGELOG.md
|
145
|
+
- Gemfile
|
146
|
+
- LICENSE.txt
|
147
|
+
- README.md
|
148
|
+
- Rakefile
|
149
|
+
- bin/console
|
150
|
+
- bin/cron_kubernetes
|
151
|
+
- bin/setup
|
152
|
+
- cron-kubernetes.gemspec
|
153
|
+
- lib/cron-kubernetes.rb
|
154
|
+
- lib/cron_kubernetes.rb
|
155
|
+
- lib/cron_kubernetes/configurable.rb
|
156
|
+
- lib/cron_kubernetes/cron_job.rb
|
157
|
+
- lib/cron_kubernetes/cron_tab.rb
|
158
|
+
- lib/cron_kubernetes/kubeclient_context.rb
|
159
|
+
- lib/cron_kubernetes/kubernetes_client.rb
|
160
|
+
- lib/cron_kubernetes/scheduler.rb
|
161
|
+
- lib/cron_kubernetes/version.rb
|
162
|
+
homepage: https://github.com/keylimetoolbox/cron-kubernetes
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.7.4
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Configure and deploy Kubernetes CronJobs from ruby.
|
186
|
+
test_files: []
|