kuberun 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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
@@ -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
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
@@ -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