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 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: []