kontena-plugin-shell 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,85 @@
1
+ module Kontena::Plugin
2
+ module Shell
3
+ class Command
4
+
5
+ attr_reader :context, :args, :session
6
+
7
+ def self.command(name = nil)
8
+ return @command if instance_variable_defined?(:@command)
9
+ disabled_commands = ENV['KOSH_DISABLED_COMMANDS'].to_s.split(/,/)
10
+ Array(name).each { |name| Shell.commands[name] = self unless disabled_commands.include?(name) }
11
+ @command = name
12
+ end
13
+
14
+ def self.subcommands(subcommands = nil)
15
+ return @subcommands if instance_variable_defined?(:@subcommands)
16
+ @subcommands = {}
17
+ Array(subcommands).each do |sc|
18
+ Array(sc.command).each do |name|
19
+ @subcommands[name] = sc
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.has_subcommands?
25
+ !subcommands.nil? && !subcommands.empty?
26
+ end
27
+
28
+ def has_subcommands?
29
+ self.class.has_subcommands?
30
+ end
31
+
32
+ def self.description(text = nil)
33
+ @description ||= text
34
+ end
35
+
36
+ def self.help(text = nil)
37
+ @help ||= text
38
+ end
39
+
40
+ def self.completions(*completions)
41
+ @completions ||= completions
42
+ end
43
+
44
+ def initialize(context = nil, args = nil, session = nil)
45
+ @args = Array(args)
46
+ @context = context
47
+ @session = session
48
+ end
49
+
50
+ def run
51
+ if has_subcommands?
52
+ if args[1]
53
+ subcommand = self.class.subcommands[args[1]]
54
+ if subcommand
55
+ subcommand.new(context, args[1..-1], session).run
56
+ else
57
+ puts Kontena.pastel.red("Unknown subcommand")
58
+ end
59
+ else
60
+ context << args[0]
61
+ end
62
+ else
63
+ execute
64
+ end
65
+ rescue Kontena::Plugin::Shell::ExitCommand::CleanExit
66
+ puts Kontena.pastel.green("Bye!")
67
+ exit 0
68
+ rescue => ex
69
+ puts Kontena.pastel.red("ERROR: " + ex.message)
70
+ puts Kontena.pastel.green(ex.backtrace.join("\n ")) if ENV["DEBUG"]
71
+ end
72
+ end
73
+
74
+ # SubCommand is just like a command, except it doesn't register the
75
+ # class to Shell.commands.
76
+ class SubCommand < Command
77
+ def self.command(name = nil)
78
+ @command ||= name
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+
85
+ Dir[File.expand_path('../commands/**/*.rb', __FILE__)].each { |file| require file }
@@ -0,0 +1,29 @@
1
+ require 'kontena/plugin/shell/command'
2
+ require 'kontena/plugin/shell/commands/batch_do'
3
+
4
+ module Kontena::Plugin
5
+ module Shell
6
+ class BatchCommand < Command
7
+
8
+ command 'batch'
9
+ description 'Run/create command batches'
10
+ help -> (context, tokens) {
11
+ if tokens && tokens[2] && subcommands[tokens[2]]
12
+ subcommands[tokens[2]].help
13
+ else
14
+ <<-EOB.gsub(/^\s+/, '')
15
+ To run a series of commands in a batch, use:
16
+
17
+ > batch do
18
+ > command
19
+ > command 2
20
+ > end
21
+ EOB
22
+ end
23
+ }
24
+
25
+ subcommands [Kontena::Plugin::Shell::BatchDoCommand]
26
+ completions *subcommands.keys
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ require 'kontena/plugin/shell/command'
2
+
3
+ module Kontena::Plugin
4
+ module Shell
5
+ class BatchDoCommand < SubCommand
6
+
7
+ command 'do'
8
+ description 'Run a batch of commands'
9
+ help "Example:\n> batch do\n> master users ls\n> master ls\n> end"
10
+ completions nil
11
+
12
+ def execute
13
+ if args.size > 1
14
+ lines = args[1..-1].join(' ').split(/(?<!\\);/).map(&:strip)
15
+ else
16
+ lines = []
17
+ while buf = Readline.readline("#{Kontena.pastel.green('..')}#{Kontena.pastel.red('>')} ", true)
18
+ buf.strip!
19
+ break if buf == 'end'
20
+ lines << buf unless buf.empty?
21
+ end
22
+ (lines.size + 1).times { Readline::HISTORY.pop }
23
+ Readline::HISTORY.push "batch do #{lines.join('; ')}"
24
+ end
25
+
26
+ lines.each { |line| session.run_command(line) }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'kontena/plugin/shell/command'
2
+
3
+ module Kontena::Plugin
4
+ module Shell
5
+ class ContextTopCommand < Command
6
+ command '/'
7
+ description 'Clear context'
8
+ help "Go to top in context.\n\nYou can also directly call other commands without switching context first by using for example #{Kontena.pastel.yellow('/ master ls')}"
9
+
10
+ def execute
11
+ if args[1] && session
12
+ old_context = context.to_a.clone
13
+ context.top
14
+ session.run_command(args[1..-1].join(' '))
15
+ context.concat(old_context)
16
+ else
17
+ context.top
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require 'kontena/plugin/shell/command'
2
+
3
+ module Kontena::Plugin
4
+ module Shell
5
+ class ContextUpCommand < Command
6
+ command '..'
7
+ description 'Go up in context'
8
+ help 'Go up in context'
9
+
10
+ def execute
11
+ context.up
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ require 'kontena/plugin/shell/command'
2
+
3
+ module Kontena::Plugin
4
+ module Shell
5
+ class DebugCommand < Command
6
+ command 'debug'
7
+ description 'Toggle debug output'
8
+ help 'Use debug on/off to toggle debug output.'
9
+
10
+ completions 'on', 'off', 'api'
11
+
12
+ def execute
13
+ case args[1]
14
+ when 'true', 'on', '1'
15
+ ENV['DEBUG'] = 'true'
16
+ when 'api'
17
+ ENV['DEBUG'] = 'api'
18
+ when 'off', 'false', '0'
19
+ ENV.delete('DEBUG')
20
+ when NilClass
21
+ # do nothing
22
+ else
23
+ puts Kontena.pastel.red("Unknown argument '#{args[1]}'")
24
+ end
25
+ puts "Debug #{Kontena.pastel.send(*ENV['DEBUG'] ? [:green, 'on'] : [:red, 'off'])}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ require 'kontena/plugin/shell/command'
2
+
3
+ module Kontena::Plugin
4
+ module Shell
5
+ class ExitCommand < Command
6
+ command 'exit'
7
+ description 'Quit shell'
8
+ help 'Enter "exit" to quit.'
9
+
10
+ completions nil
11
+
12
+ CleanExit = Class.new(StandardError)
13
+
14
+ def execute
15
+ raise CleanExit
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,43 @@
1
+ require 'kontena/plugin/shell/command'
2
+
3
+ module Kontena::Plugin
4
+ module Shell
5
+ class HelpCommand < Command
6
+ command 'help'
7
+ description 'Show help'
8
+ help 'Use "help <command>" to see help for a specific command'
9
+ #completions -> (context, tokens, word) { Kontena::Completer.complete(context.to_a + tokens) }
10
+
11
+ def cmd
12
+ full_line = context + args[1..-1]
13
+ cmd = Shell.command(full_line.first) || Shell.command('kontena')
14
+ end
15
+
16
+ def execute
17
+ if cmd.help.respond_to?(:call)
18
+ help_text = cmd.help.call(context, args[1..-1])
19
+ else
20
+ help_text = cmd.help
21
+ end
22
+ puts help_text
23
+
24
+ if cmd.has_subcommands?
25
+ puts
26
+ puts Kontena.pastel.green("Subcommands:")
27
+ cmd.subcommands.each do |name, sc|
28
+ puts sprintf(' %-29s %s', name, sc.description)
29
+ end
30
+ puts
31
+ end
32
+
33
+ if args.empty? || (args.size == 1 && args.first == 'help')
34
+ puts Kontena.pastel.green('KOSH commands:')
35
+ Shell.commands.each do |name, cmd|
36
+ next if cmd == Kontena::Plugin::Shell::KontenaCommand
37
+ puts sprintf(' %-29s %s', name, cmd.description)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,57 @@
1
+ require 'kontena/main_command' unless Kontena.const_defined?(:MainCommand)
2
+ require 'kontena/plugin/shell/command'
3
+ require 'kontena/plugin/shell/completer'
4
+ require 'kontena/plugin/shell/context'
5
+
6
+ module Kontena::Plugin
7
+ module Shell
8
+ class KontenaCommand < Command
9
+
10
+ command ['kontena'] + Kontena::MainCommand.recognised_subcommands.flat_map(&:names)
11
+ description 'Run Kontena-cli command'
12
+ help -> (context, tokens) { new(context, tokens).subcommand_class.help('').gsub(/^(\s+)\[OPTIONS\] SUB/, "\\1 SUB") }
13
+ completions -> (context, tokens, word) { Kontena::Plugin::Shell::Completer.complete(context.to_a + tokens) }
14
+
15
+ def cmd
16
+ cmd = Kontena::MainCommand.new('')
17
+ cmdline = context.to_a + args
18
+ cmdline.shift if cmdline.first == 'kontena'
19
+ cmd.parse(cmdline)
20
+ cmd
21
+ end
22
+
23
+ def execute
24
+ cmd.run([])
25
+ rescue Clamp::HelpWanted => ex
26
+ unless args.include?('--help') || args.include?('-h')
27
+ context.concat(args)
28
+ end
29
+ rescue SystemExit => ex
30
+ puts Kontena.pastel.red('[Command exited with error]') unless ex.status.zero?
31
+ rescue => ex
32
+ puts Kontena.pastel.red("ERROR: #{ex.message}")
33
+ end
34
+
35
+ def subcommand_class
36
+ (context + args).reject { |t| t.start_with?('-') }.inject(Kontena::MainCommand) do |base, token|
37
+ if base.has_subcommands?
38
+ sc = base.recognised_subcommands.find { |sc| sc.names.include?(token) }
39
+ sc ? sc.subcommand_class : base
40
+ else
41
+ base
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ if ENV["KOSH_DISABLED_COMMANDS"]
50
+ ENV["KOSH_DISABLED_COMMANDS"].split(',').each do |command|
51
+ tokens = command.split(' ')
52
+ subcommand = tokens.pop
53
+ kontena = Kontena::Plugin::Shell::KontenaCommand.new(Kontena::Plugin::Shell::Context.new(tokens))
54
+ found_subcommand = kontena.subcommand_class
55
+ found_subcommand.recognised_subcommands.delete_if { |sc| sc.names.include?(subcommand) }
56
+ end
57
+ end
@@ -0,0 +1,216 @@
1
+ # this is mostly just a wrapped version of the completer in cli
2
+ require 'kontena/cli/common' unless Kontena.const_defined?(:Cli) && Kontena::Cli.const_defined?(:Common)
3
+ require 'yaml'
4
+
5
+ module Kontena::Plugin::Shell
6
+ module Completer
7
+ class Helper
8
+ include Kontena::Cli::Common
9
+
10
+ def grids
11
+ client.get("grids")['grids'].map{|grid| grid['id']}
12
+ rescue
13
+ []
14
+ end
15
+
16
+ def nodes
17
+ client.get("grids/#{current_grid}/nodes")['nodes'].map{|node| node['name']}
18
+ rescue
19
+ []
20
+ end
21
+
22
+ def stacks
23
+ stacks = client.get("grids/#{current_grid}/stacks")['stacks']
24
+ results = []
25
+ results.push stacks.map{|s| s['name']}
26
+ results.delete('null')
27
+ results
28
+ rescue
29
+ []
30
+ end
31
+
32
+ def services
33
+ services = client.get("grids/#{current_grid}/services")['services']
34
+ results = []
35
+ results.push services.map{ |s|
36
+ stack = s['stack']['id'].split('/').last
37
+ if stack != 'null'
38
+ "#{stack}/#{s['name']}"
39
+ else
40
+ s['name']
41
+ end
42
+ }
43
+ results
44
+ rescue
45
+ []
46
+ end
47
+
48
+ def containers
49
+ results = []
50
+ client.get("grids/#{current_grid}/services")['services'].each do |service|
51
+ containers = client.get("services/#{service['id']}/containers")['containers']
52
+ results.push(containers.map{|c| c['name'] })
53
+ results.push(containers.map{|c| c['id'] })
54
+ end
55
+ results
56
+ rescue
57
+ []
58
+ end
59
+
60
+ def yml_services
61
+ if File.exist?('kontena.yml')
62
+ yaml = YAML.safe_load(File.read('kontena.yml'))
63
+ services = yaml['services']
64
+ services.keys
65
+ end
66
+ rescue
67
+ []
68
+ end
69
+
70
+ def yml_files
71
+ Dir["./*.yml"].map{|file| file.sub('./', '')}
72
+ rescue
73
+ []
74
+ end
75
+
76
+ def master_names
77
+ config_file = File.expand_path('~/.kontena_client.json')
78
+ if(File.exist?(config_file))
79
+ config = JSON.parse(File.read(config_file))
80
+ return config['servers'].map{|s| s['name']}
81
+ end
82
+ rescue
83
+ []
84
+ end
85
+ end
86
+
87
+ def self.complete(words)
88
+ while words.first == 'kontena' || words.first == 'complete'
89
+ words.shift
90
+ end
91
+ helper = Helper.new
92
+ completion = []
93
+
94
+ case words[0]
95
+ when NilClass, ''
96
+ completion.concat %w(cloud logout grid app service stack vault certificate node master vpn registry container etcd external-registry whoami plugin version)
97
+ when 'plugin'
98
+ sub_commands = %w(list ls search install uninstall)
99
+ if words[1]
100
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
101
+ else
102
+ completion.push sub_commands
103
+ end
104
+ when 'etcd'
105
+ sub_commands = %w(get set mkdir mk list ls rm)
106
+ if words[1]
107
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
108
+ else
109
+ completion.push sub_commands
110
+ end
111
+ when 'registry'
112
+ sub_commands = %w(create remove rm)
113
+ if words[1]
114
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
115
+ else
116
+ completion.push sub_commands
117
+ end
118
+ when 'grid'
119
+ sub_commands = %w(add-user audit-log create current list user remove show use)
120
+ if words[1]
121
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
122
+ completion.push helper.grids
123
+ else
124
+ completion.push sub_commands
125
+ end
126
+ when 'node'
127
+ sub_commands = %w(list show remove)
128
+ if words[1]
129
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
130
+ completion.push helper.nodes
131
+ else
132
+ completion.push sub_commands
133
+ end
134
+ when 'master'
135
+ sub_commands = %w(list use users current remove rm config cfg login logout token join audit-log init-cloud)
136
+ if words[1] && words[1] == 'use'
137
+ completion.push helper.master_names
138
+ elsif words[1] && words[1] == 'users'
139
+ users_sub_commands = %(invite list role)
140
+ completion.push users_sub_commands
141
+ elsif words[1] && ['config', 'cfg'].include?(words[1])
142
+ config_sub_commands = %(set get dump load import export unset)
143
+ completion.push config_sub_commands
144
+ elsif words[1] && words[1] == 'token'
145
+ token_sub_commands = %(list ls rm remove show current create)
146
+ completion.push token_sub_commands
147
+ elsif words[1]
148
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
149
+ else
150
+ completion.push sub_commands
151
+ end
152
+ when 'cloud'
153
+ sub_commands = %w(login logout master)
154
+ if words[1] && words[1] == 'master'
155
+ cloud_master_sub_commands = %(list ls remove rm add show update)
156
+ completion.push cloud_master_sub_commands
157
+ elsif words[1]
158
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
159
+ else
160
+ completion.push sub_commands
161
+ end
162
+ when 'service'
163
+ sub_commands = %w(containers create delete deploy list logs restart
164
+ scale show start stats stop update monitor env
165
+ secret link unlink)
166
+ if words[1]
167
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
168
+ completion.push helper.services
169
+ else
170
+ completion.push sub_commands
171
+ end
172
+ when 'container'
173
+ sub_commands = %w(exec inspect logs)
174
+ if words[1]
175
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
176
+ completion.push helper.containers
177
+ else
178
+ completion.push sub_commands
179
+ end
180
+ when 'vpn'
181
+ completion.push %w(config create delete)
182
+ when 'external-registry'
183
+ completion.push %w(add list delete)
184
+ when 'app'
185
+ sub_commands = %w(init build config deploy start stop remove rm ps list
186
+ logs monitor show)
187
+ if words[1]
188
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
189
+ completion.push helper.yml_services
190
+ else
191
+ completion.push sub_commands
192
+ end
193
+ when 'stack'
194
+ sub_commands = %w(build install upgrade deploy start stop remove rm ls list
195
+ logs monitor show registry)
196
+ if words[1]
197
+ if words[1] == 'registry'
198
+ registry_sub_commands = %(push pull search show rm)
199
+ completion.push registry_sub_commands
200
+ elsif %w(install).include?(words[1])
201
+ completion.push helper.yml_files
202
+ elsif words[1] == 'upgrade' && words[3]
203
+ completion.push helper.yml_files
204
+ else
205
+ completion.push(sub_commands) unless sub_commands.include?(words[1])
206
+ completion.push helper.stacks
207
+ end
208
+ else
209
+ completion.push sub_commands
210
+ end
211
+ else
212
+ end
213
+ completion.flatten.flat_map { |item| item.split(/\s+/) }
214
+ end
215
+ end
216
+ end