kuberun 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +4 -0
- data/.gitignore +68 -0
- data/.rspec +3 -0
- data/.rubocop.yml +15 -0
- data/.rubocop_todo.yml +24 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +149 -0
- data/LICENSE.txt +20 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/kuberun +19 -0
- data/kuberun.gemspec +49 -0
- data/lib/kuberun.rb +16 -0
- data/lib/kuberun/cli.rb +50 -0
- data/lib/kuberun/command.rb +125 -0
- data/lib/kuberun/commands/.gitkeep +1 -0
- data/lib/kuberun/commands/run_pod.rb +109 -0
- data/lib/kuberun/kubectl.rb +106 -0
- data/lib/kuberun/templates/.gitkeep +1 -0
- data/lib/kuberun/templates/run_pod/.gitkeep +1 -0
- data/lib/kuberun/version.rb +5 -0
- metadata +393 -0
data/Rakefile
ADDED
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 'kuberun'
|
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/setup
ADDED
data/exe/kuberun
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib_path = File.expand_path('../lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
6
|
+
require 'kuberun/cli'
|
7
|
+
require 'kuberun'
|
8
|
+
|
9
|
+
Signal.trap('INT') do
|
10
|
+
warn("\n#{caller.join("\n")}: interrupted")
|
11
|
+
exit(1)
|
12
|
+
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
Kuberun::CLI.start
|
16
|
+
rescue Kuberun::CLI::Error => err
|
17
|
+
puts "ERROR: #{err.message}"
|
18
|
+
exit 1
|
19
|
+
end
|
data/kuberun.gemspec
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'kuberun/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'kuberun'
|
9
|
+
spec.license = 'MIT'
|
10
|
+
spec.version = Kuberun::VERSION
|
11
|
+
spec.authors = ['kruczjak']
|
12
|
+
spec.email = ['jakub.kruczek@boostcom.no']
|
13
|
+
|
14
|
+
spec.summary = 'CLI to run pods based on deployments'
|
15
|
+
spec.description = 'CLI to run pods based on deployments'
|
16
|
+
spec.homepage = 'https://boostcom.com'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'pastel', '~> 0.7.2'
|
26
|
+
spec.add_dependency 'thor', '~> 0.20.0'
|
27
|
+
spec.add_dependency 'tty-color', '~> 0.4.2'
|
28
|
+
spec.add_dependency 'tty-command', '~> 0.8.0'
|
29
|
+
spec.add_dependency 'tty-config', '~> 0.2.0'
|
30
|
+
spec.add_dependency 'tty-cursor', '~> 0.5.0'
|
31
|
+
spec.add_dependency 'tty-editor', '~> 0.4.0'
|
32
|
+
spec.add_dependency 'tty-file', '~> 0.6.0'
|
33
|
+
spec.add_dependency 'tty-font', '~> 0.2.0'
|
34
|
+
spec.add_dependency 'tty-markdown', '~> 0.4.0'
|
35
|
+
spec.add_dependency 'tty-pager', '~> 0.11.0'
|
36
|
+
spec.add_dependency 'tty-platform', '~> 0.1.0'
|
37
|
+
spec.add_dependency 'tty-progressbar', '~> 0.15.0'
|
38
|
+
spec.add_dependency 'tty-prompt', '~> 0.16.1'
|
39
|
+
spec.add_dependency 'tty-screen', '~> 0.6.4'
|
40
|
+
spec.add_dependency 'tty-spinner', '~> 0.8.0'
|
41
|
+
spec.add_dependency 'tty-table', '~> 0.10.0'
|
42
|
+
spec.add_dependency 'tty-tree', '~> 0.1.0'
|
43
|
+
spec.add_dependency 'tty-which', '~> 0.3.0'
|
44
|
+
|
45
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
46
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
47
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
48
|
+
spec.add_development_dependency 'rubocop', '~> 0.58.0'
|
49
|
+
end
|
data/lib/kuberun.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kuberun/version'
|
4
|
+
|
5
|
+
require 'pastel'
|
6
|
+
require 'tty-command'
|
7
|
+
require 'tty-prompt'
|
8
|
+
|
9
|
+
require 'kuberun/kubectl'
|
10
|
+
require 'json'
|
11
|
+
|
12
|
+
module Kuberun
|
13
|
+
Kubectl = ::Kubectl.new
|
14
|
+
Pastel = ::Pastel.new
|
15
|
+
# Your code goes here...
|
16
|
+
end
|
data/lib/kuberun/cli.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module Kuberun
|
6
|
+
# Handle the application command line parsing
|
7
|
+
# and the dispatch to various command objects
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class CLI < Thor
|
11
|
+
DEFAULT_OPTIONS_FOR_KUBECTL_OPTIONS = { type: :string, default: '', desc: 'See kubectl options' }
|
12
|
+
BASE_KUBECTL_OPTIONS = {
|
13
|
+
'certificate-authority': {},
|
14
|
+
'client-certificate': {},
|
15
|
+
'client-key': {},
|
16
|
+
'cluster': {},
|
17
|
+
'context': {},
|
18
|
+
'insecure-skip-tls-verify': {},
|
19
|
+
'kubeconfig': {},
|
20
|
+
'namespace': { aliases: :'-n' },
|
21
|
+
'token': {},
|
22
|
+
'v': { type: :numeric, default: 0, desc: 'Log level, also passed to kubectl' },
|
23
|
+
}
|
24
|
+
BASE_KUBECTL_OPTIONS.each do |option_name, hash|
|
25
|
+
class_option option_name, DEFAULT_OPTIONS_FOR_KUBECTL_OPTIONS.merge(hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Error raised by this runner
|
29
|
+
Error = Class.new(StandardError)
|
30
|
+
|
31
|
+
desc 'version', 'kuberun version'
|
32
|
+
def version
|
33
|
+
require_relative 'version'
|
34
|
+
puts "v#{Kuberun::VERSION}"
|
35
|
+
end
|
36
|
+
map %w[--version -v] => :version
|
37
|
+
|
38
|
+
desc 'run_pod DEPLOYMENT_NAME', 'Starts pod for command'
|
39
|
+
method_option :help, aliases: '-h', type: :boolean,
|
40
|
+
desc: 'Display usage information'
|
41
|
+
def run_pod(deployment_name)
|
42
|
+
if options[:help]
|
43
|
+
invoke :help, ['run_pod']
|
44
|
+
else
|
45
|
+
require_relative 'commands/run_pod'
|
46
|
+
Kuberun::Commands::RunPod.new(deployment_name, options).execute
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Kuberun
|
6
|
+
class Command
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :command, :run
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
Kuberun::Kubectl.load_options(options)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Execute this command
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def execute(*)
|
19
|
+
raise(
|
20
|
+
NotImplementedError,
|
21
|
+
"#{self.class}##{__method__} must be implemented"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# The external commands runner
|
26
|
+
#
|
27
|
+
# @see http://www.rubydoc.info/gems/tty-command
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def command(**options)
|
31
|
+
require 'tty-command'
|
32
|
+
TTY::Command.new(options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The cursor movement
|
36
|
+
#
|
37
|
+
# @see http://www.rubydoc.info/gems/tty-cursor
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def cursor
|
41
|
+
require 'tty-cursor'
|
42
|
+
TTY::Cursor
|
43
|
+
end
|
44
|
+
|
45
|
+
# Open a file or text in the user's preferred editor
|
46
|
+
#
|
47
|
+
# @see http://www.rubydoc.info/gems/tty-editor
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
def editor
|
51
|
+
require 'tty-editor'
|
52
|
+
TTY::Editor
|
53
|
+
end
|
54
|
+
|
55
|
+
# File manipulation utility methods
|
56
|
+
#
|
57
|
+
# @see http://www.rubydoc.info/gems/tty-file
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
def generator
|
61
|
+
require 'tty-file'
|
62
|
+
TTY::File
|
63
|
+
end
|
64
|
+
|
65
|
+
# Terminal output paging
|
66
|
+
#
|
67
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
68
|
+
#
|
69
|
+
# @api public
|
70
|
+
def pager(**options)
|
71
|
+
require 'tty-pager'
|
72
|
+
TTY::Pager.new(options)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Terminal platform and OS properties
|
76
|
+
#
|
77
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def platform
|
81
|
+
require 'tty-platform'
|
82
|
+
TTY::Platform.new
|
83
|
+
end
|
84
|
+
|
85
|
+
# The interactive prompt
|
86
|
+
#
|
87
|
+
# @see http://www.rubydoc.info/gems/tty-prompt
|
88
|
+
#
|
89
|
+
# @api public
|
90
|
+
def prompt(**options)
|
91
|
+
require 'tty-prompt'
|
92
|
+
TTY::Prompt.new(options)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get terminal screen properties
|
96
|
+
#
|
97
|
+
# @see http://www.rubydoc.info/gems/tty-screen
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def screen
|
101
|
+
require 'tty-screen'
|
102
|
+
TTY::Screen
|
103
|
+
end
|
104
|
+
|
105
|
+
# The unix which utility
|
106
|
+
#
|
107
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def which(*args)
|
111
|
+
require 'tty-which'
|
112
|
+
TTY::Which.which(*args)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if executable exists
|
116
|
+
#
|
117
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
118
|
+
#
|
119
|
+
# @api public
|
120
|
+
def exec_exist?(*args)
|
121
|
+
require 'tty-which'
|
122
|
+
TTY::Which.exist?(*args)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Kuberun
|
6
|
+
module Commands
|
7
|
+
class RunPod < Kuberun::Command
|
8
|
+
NEW_POD = 'Start new one'
|
9
|
+
|
10
|
+
def initialize(deployment_name, options)
|
11
|
+
@deployment_name = deployment_name
|
12
|
+
@options = options
|
13
|
+
super(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(input: $stdin, output: $stdout)
|
17
|
+
output.puts(Kuberun::Pastel.yellow('Checking access to needed commands...'))
|
18
|
+
Kuberun::Kubectl.auth_check('create', resource: 'pods')
|
19
|
+
Kuberun::Kubectl.auth_check('exec', resource: 'pods')
|
20
|
+
output.puts(Kuberun::Pastel.green('You have all permissions needed.'))
|
21
|
+
|
22
|
+
output.puts(Kuberun::Pastel.yellow('Searching for existing pods'))
|
23
|
+
existing_pods = Kuberun::Kubectl.get(resource: 'pods', options: "-l kuberun-provisioned=true,kuberun-source=#{@deployment_name}")
|
24
|
+
if existing_pods['items'].size > 0
|
25
|
+
select = existing_pods['items'].map { |item| item.dig('metadata', 'name') }
|
26
|
+
select << NEW_POD
|
27
|
+
|
28
|
+
selection = prompt.select('I found some already running pods. Do you want to use one?', select)
|
29
|
+
|
30
|
+
if selection == NEW_POD
|
31
|
+
create_pod_from_deployment(output)
|
32
|
+
else
|
33
|
+
@generated_pod_name = selection
|
34
|
+
end
|
35
|
+
else
|
36
|
+
create_pod_from_deployment(output)
|
37
|
+
end
|
38
|
+
|
39
|
+
execute_command(input, output)
|
40
|
+
|
41
|
+
unless prompt.no?('Should I delete pod?')
|
42
|
+
Kuberun::Kubectl.delete(resource: 'pod', resource_name: generated_pod_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
output.puts(Kuberun::Pastel.green('Done!'))
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def create_pod_from_deployment(output)
|
51
|
+
deployment = Kuberun::Kubectl.get(resource: 'deployment', resource_name: @deployment_name, options: '--export')
|
52
|
+
pod_template = deployment['spec']['template']
|
53
|
+
prepare_pod_template(pod_template)
|
54
|
+
|
55
|
+
Kuberun::Kubectl.create(configuration: pod_template)
|
56
|
+
wait_while do
|
57
|
+
pod = Kuberun::Kubectl.get(resource: 'pod', resource_name: generated_pod_name)
|
58
|
+
pod.dig('status', 'phase') == 'Running'
|
59
|
+
end
|
60
|
+
|
61
|
+
output.puts(Kuberun::Pastel.green('Pod is running!'))
|
62
|
+
end
|
63
|
+
|
64
|
+
def prepare_pod_template(pod_template)
|
65
|
+
pod_template['apiVersion'] = 'v1'
|
66
|
+
pod_template['kind'] = 'Pod'
|
67
|
+
pod_template['metadata']['name'] = generated_pod_name
|
68
|
+
|
69
|
+
pod_template['metadata']['labels'] = {
|
70
|
+
'kuberun' => Kuberun::VERSION.to_s,
|
71
|
+
'kuberun-provisioned' => 'true',
|
72
|
+
'kuberun-source' => @deployment_name,
|
73
|
+
}
|
74
|
+
|
75
|
+
pod_template['spec']['containers'][0].delete('livenessProbe')
|
76
|
+
pod_template['spec']['containers'][0].delete('readinessProbe')
|
77
|
+
pod_template['spec'].delete('affinity')
|
78
|
+
end
|
79
|
+
|
80
|
+
def wait_while
|
81
|
+
loop do
|
82
|
+
begin
|
83
|
+
status = yield
|
84
|
+
raise 'Not ok' unless status
|
85
|
+
break
|
86
|
+
rescue
|
87
|
+
sleep(1)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def generated_pod_name
|
93
|
+
@generated_pod_name ||= "kuberun-#{@deployment_name}-#{Time.now.to_i}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def execute_command(_input, output)
|
97
|
+
output.puts(Kuberun::Pastel.green('Executing command'))
|
98
|
+
|
99
|
+
Kuberun::Kubectl.exec(pod: generated_pod_name, command: '-it /bin/sh')
|
100
|
+
|
101
|
+
output.puts(Kuberun::Pastel.green('Kubectl exec exited'))
|
102
|
+
end
|
103
|
+
|
104
|
+
def prompt
|
105
|
+
TTY::Prompt.new
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'pty'
|
5
|
+
|
6
|
+
class Kubectl
|
7
|
+
CAT_MULTILINE = 'EOFCONFIG'
|
8
|
+
KUBECTL_OPTIONS = Kuberun::CLI::BASE_KUBECTL_OPTIONS.keys.map(&:to_s)
|
9
|
+
|
10
|
+
def load_options(options)
|
11
|
+
self.kubectl_options = parsed_options(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def auth_check(verb, resource:, resource_name: nil)
|
15
|
+
cmd.run(kubectl_base_command("auth can-i #{verb}", resource: resource, resource_name: resource_name))
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(resource:, resource_name: nil, options: nil)
|
19
|
+
options = "#{options} --output=json"
|
20
|
+
parsed_json do
|
21
|
+
cmd.run(kubectl_base_command('get', resource: resource, resource_name: resource_name, options: options)).out
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(configuration:, options: nil)
|
26
|
+
cmd.run(kubectl_base_input_command('create', configuration: configuration, options: options))
|
27
|
+
end
|
28
|
+
|
29
|
+
def exec(pod:, command:)
|
30
|
+
old_state = `stty -g`
|
31
|
+
|
32
|
+
PTY.spawn("#{kubectl_base_command('exec', resource: pod)} #{command}") do |o, i, pid|
|
33
|
+
t_in = Thread.new do
|
34
|
+
until i.closed? do
|
35
|
+
input = $stdin.getch
|
36
|
+
i.write(input)
|
37
|
+
i.flush
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
t_out = Thread.new do
|
42
|
+
begin
|
43
|
+
until o.eof? do
|
44
|
+
$stdout.print(o.readchar)
|
45
|
+
$stdout.flush
|
46
|
+
end
|
47
|
+
rescue Errno::EIO, EOFError
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
t_in.run
|
53
|
+
|
54
|
+
Process::waitpid(pid) rescue nil
|
55
|
+
# "rescue nil" is there in case process already ended.
|
56
|
+
|
57
|
+
t_out.join
|
58
|
+
t_in.kill
|
59
|
+
sleep 0.1
|
60
|
+
ensure
|
61
|
+
t_out&.kill
|
62
|
+
t_in&.kill
|
63
|
+
$stdout.puts
|
64
|
+
$stdout.flush
|
65
|
+
system "stty #{ old_state }"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete(resource:, resource_name:)
|
70
|
+
cmd.run(kubectl_base_command('delete', resource: resource, resource_name: resource_name))
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
attr_accessor :kubectl_options
|
76
|
+
|
77
|
+
def cmd(tty_options = {})
|
78
|
+
TTY::Command.new(tty_options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def kubectl_base_command(verb, resource:, resource_name: nil, options: nil)
|
82
|
+
base = "#{kubectl} #{options} #{verb} #{resource}"
|
83
|
+
base = "#{base}/#{resource_name}" if resource_name
|
84
|
+
base
|
85
|
+
end
|
86
|
+
|
87
|
+
def kubectl_base_input_command(verb, configuration:, options:)
|
88
|
+
"cat << '#{CAT_MULTILINE}' | #{kubectl} #{options} #{verb} -f - 2>&1\n#{configuration.to_json}\n#{CAT_MULTILINE}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def kubectl
|
92
|
+
"kubectl #{kubectl_options}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def parsed_options(options)
|
96
|
+
options.each_with_object([]) do |(option_name, option_value), arr|
|
97
|
+
next if !KUBECTL_OPTIONS.include?(option_name) || option_value == ''
|
98
|
+
|
99
|
+
arr << "--#{option_name}=#{option_value}"
|
100
|
+
end.join(' ')
|
101
|
+
end
|
102
|
+
|
103
|
+
def parsed_json
|
104
|
+
JSON.load(yield)
|
105
|
+
end
|
106
|
+
end
|