cron-kubernetes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /Gemfile.lock
10
+
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ - Initial Release
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gemspec
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__)
@@ -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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CronKubernetes
4
+ VERSION = "0.1.0"
5
+ 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: []