kuberun 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.
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