kontena-cli 0.0.1
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 +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +191 -0
- data/README.md +40 -0
- data/Rakefile +2 -0
- data/bin/kontena +14 -0
- data/kontena-cli.gemspec +27 -0
- data/lib/kontena/cli/commands.rb +19 -0
- data/lib/kontena/cli/common.rb +83 -0
- data/lib/kontena/cli/containers/commands.rb +12 -0
- data/lib/kontena/cli/containers/containers.rb +17 -0
- data/lib/kontena/cli/grids/audit_log.rb +20 -0
- data/lib/kontena/cli/grids/commands.rb +89 -0
- data/lib/kontena/cli/grids/grids.rb +105 -0
- data/lib/kontena/cli/grids/users.rb +32 -0
- data/lib/kontena/cli/nodes/commands.rb +27 -0
- data/lib/kontena/cli/nodes/nodes.rb +61 -0
- data/lib/kontena/cli/server/commands.rb +92 -0
- data/lib/kontena/cli/server/server.rb +45 -0
- data/lib/kontena/cli/server/user.rb +111 -0
- data/lib/kontena/cli/services/commands.rb +131 -0
- data/lib/kontena/cli/services/containers.rb +24 -0
- data/lib/kontena/cli/services/logs.rb +26 -0
- data/lib/kontena/cli/services/services.rb +196 -0
- data/lib/kontena/cli/services/stats.rb +79 -0
- data/lib/kontena/cli/version.rb +5 -0
- data/lib/kontena/client.rb +177 -0
- data/lib/kontena/errors.rb +11 -0
- metadata +157 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require_relative '../common'
|
3
|
+
|
4
|
+
module Kontena::Cli::Server
|
5
|
+
class User
|
6
|
+
include Kontena::Cli::Common
|
7
|
+
|
8
|
+
def login
|
9
|
+
require_api_url
|
10
|
+
username = ask("Email: ")
|
11
|
+
password = password("Password: ")
|
12
|
+
params = {
|
13
|
+
username: username,
|
14
|
+
password: password,
|
15
|
+
grant_type: 'password',
|
16
|
+
scope: 'user'
|
17
|
+
}
|
18
|
+
|
19
|
+
response = client.post('auth', params)
|
20
|
+
|
21
|
+
if response
|
22
|
+
inifile['server']['token'] = response['access_token']
|
23
|
+
inifile.save(filename: ini_filename)
|
24
|
+
print color('Login Successful', :green)
|
25
|
+
true
|
26
|
+
else
|
27
|
+
print color('Login Failed', :red)
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def logout
|
33
|
+
inifile['server'].delete('token')
|
34
|
+
inifile.save(filename: ini_filename)
|
35
|
+
end
|
36
|
+
|
37
|
+
def whoami
|
38
|
+
require_api_url
|
39
|
+
puts "Server: #{inifile['server']['url']}"
|
40
|
+
token = require_token
|
41
|
+
response = client(token).get('user')
|
42
|
+
puts "User: #{response['email']}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def invite(email)
|
46
|
+
require_api_url
|
47
|
+
token = require_token
|
48
|
+
data = { email: email }
|
49
|
+
response = client(token).post('users', data)
|
50
|
+
puts 'User invited' if response
|
51
|
+
end
|
52
|
+
|
53
|
+
def register
|
54
|
+
require_api_url
|
55
|
+
email = ask("Email: ")
|
56
|
+
password = password("Password: ")
|
57
|
+
password2 = password("Password again: ")
|
58
|
+
if password != password2
|
59
|
+
raise ArgumentError.new("Passwords don't match")
|
60
|
+
end
|
61
|
+
params = {email: email, password: password}
|
62
|
+
client.post('users/register', params)
|
63
|
+
end
|
64
|
+
|
65
|
+
def verify_account(token)
|
66
|
+
require_api_url
|
67
|
+
|
68
|
+
params = {token: token}
|
69
|
+
client.post('user/email_confirm', params)
|
70
|
+
print color('Account verified', :green)
|
71
|
+
end
|
72
|
+
|
73
|
+
def request_password_reset(email)
|
74
|
+
require_api_url
|
75
|
+
|
76
|
+
params = {email: email}
|
77
|
+
client.post('user/password_reset', params)
|
78
|
+
puts 'Email with password reset instructions is sent to your email address. Please follow the instructions to change your password.'
|
79
|
+
end
|
80
|
+
|
81
|
+
def reset_password(token)
|
82
|
+
require_api_url
|
83
|
+
password = password("Password: ")
|
84
|
+
password2 = password("Password again: ")
|
85
|
+
if password != password2
|
86
|
+
raise ArgumentError.new("Passwords don't match")
|
87
|
+
end
|
88
|
+
params = {token: token, password: password}
|
89
|
+
client.put('user/password_reset', params)
|
90
|
+
puts 'Password is now changed. To login with the new password, please run: kontena login'
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_registry
|
94
|
+
default_url = 'https://index.docker.io/v1/'
|
95
|
+
require_api_url
|
96
|
+
username = ask("Username: ")
|
97
|
+
password = password("Password: ")
|
98
|
+
email = ask("Email: ")
|
99
|
+
url = ask("URL [#{default_url}]: ")
|
100
|
+
url = default_url if url.strip == ''
|
101
|
+
data = { username: username, password: password, email: email, url: url }
|
102
|
+
client(token).post("user/registries", data)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def token
|
108
|
+
@token ||= require_token
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Kontena::Cli::Services; end;
|
2
|
+
|
3
|
+
require_relative 'containers'
|
4
|
+
require_relative 'logs'
|
5
|
+
require_relative 'services'
|
6
|
+
require_relative 'stats'
|
7
|
+
|
8
|
+
command 'service list' do |c|
|
9
|
+
c.syntax = 'kontena service list'
|
10
|
+
c.description = 'List all services'
|
11
|
+
c.action do |args, options|
|
12
|
+
Kontena::Cli::Services::Services.new.list
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
command 'service show' do |c|
|
17
|
+
c.syntax = 'kontena service show <service_id>'
|
18
|
+
c.description = 'Show service details'
|
19
|
+
c.action do |args, options|
|
20
|
+
Kontena::Cli::Services::Services.new.show(args[0])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
command 'service deploy' do |c|
|
25
|
+
c.syntax = 'kontena service deploy <service_id>'
|
26
|
+
c.description = 'Deploy service to nodes'
|
27
|
+
c.action do |args, options|
|
28
|
+
Kontena::Cli::Services::Services.new.deploy(args[0])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
command 'service restart' do |c|
|
33
|
+
c.syntax = 'kontena service restart <service_id>'
|
34
|
+
c.description = 'Restart service containers'
|
35
|
+
c.action do |args, options|
|
36
|
+
Kontena::Cli::Services::Services.new.restart(args[0])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
command 'service stop' do |c|
|
41
|
+
c.syntax = 'kontena service stop <service_id>'
|
42
|
+
c.description = 'Stop service containers'
|
43
|
+
c.action do |args, options|
|
44
|
+
Kontena::Cli::Services::Services.new.stop(args[0])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
command 'service start' do |c|
|
49
|
+
c.syntax = 'kontena service start <service_id>'
|
50
|
+
c.description = 'Start service containers'
|
51
|
+
c.action do |args, options|
|
52
|
+
Kontena::Cli::Services::Services.new.start(args[0])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
command 'service create' do |c|
|
57
|
+
c.syntax = 'kontena service create <name> <image>'
|
58
|
+
c.description = 'Create new service'
|
59
|
+
c.option '-p', '--ports Array', Array, 'Publish a service\'s port to the host'
|
60
|
+
c.option '-e', '--env Array', Array, 'Set environment variables'
|
61
|
+
c.option '-l', '--link Array', Array, 'Add link to another service in the form of name:alias'
|
62
|
+
c.option '-v', '--volume Array', Array, 'Mount a volume'
|
63
|
+
c.option '--volumes_from Array', Array, 'Mount volumes from another container'
|
64
|
+
c.option '-a', '--affinity Array', Array, 'Set service affinity'
|
65
|
+
c.option '-c', '--cpu-shares INTEGER', Integer, 'CPU shares (relative weight)'
|
66
|
+
c.option '-m', '--memory INTEGER', String, 'Memory limit (format: <number><optional unit>, where unit = b, k, m or g)'
|
67
|
+
c.option '--memory-swap INTEGER', String, 'Total memory usage (memory + swap), set \'-1\' to disable swap (format: <number><optional unit>, where unit = b, k, m or g)'
|
68
|
+
c.option '--cmd STRING', String, 'Command to execute'
|
69
|
+
c.option '--instances INTEGER', Integer, 'How many instances should be deployed'
|
70
|
+
c.option '-u', '--user String', String, 'Username who executes first process inside container'
|
71
|
+
c.option '--stateful', 'Set service as stateful'
|
72
|
+
|
73
|
+
c.action do |args, options|
|
74
|
+
Kontena::Cli::Services::Services.new.create(args[0], args[1], options)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
command 'service update' do |c|
|
79
|
+
c.syntax = 'kontena service update <service_id>'
|
80
|
+
c.description = 'Update service'
|
81
|
+
c.option '-p', '--ports Array', Array, 'Exposed ports'
|
82
|
+
c.option '-e', '--env Array', Array, 'Environment variables'
|
83
|
+
c.option '--image STRING', String, 'Service image'
|
84
|
+
c.option '--instances INTEGER', Integer, 'How many instances should be deployed'
|
85
|
+
c.option '--cmd STRING', String, 'Command to execute'
|
86
|
+
c.action do |args, options|
|
87
|
+
Kontena::Cli::Services::Services.new.update(args[0], options)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
command 'service scale' do |c|
|
92
|
+
c.syntax = 'kontena service scale <service_id> <instances>'
|
93
|
+
c.description = 'Scale service horizontally'
|
94
|
+
c.action do |args, options|
|
95
|
+
Kontena::Cli::Services::Services.new.scale(args[0], args[1])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
command 'service delete' do |c|
|
100
|
+
c.syntax = 'kontena service delete <service_id>'
|
101
|
+
c.description = 'Delete service'
|
102
|
+
c.action do |args, options|
|
103
|
+
Kontena::Cli::Services::Services.new.destroy(args[0])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
command 'service containers' do |c|
|
108
|
+
c.syntax = 'kontena service containers <service_id>'
|
109
|
+
c.description = 'Show service containers'
|
110
|
+
c.action do |args, options|
|
111
|
+
Kontena::Cli::Services::Containers.new.list(args[0])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
command 'service logs' do |c|
|
116
|
+
c.syntax = 'kontena service logs <service_id>'
|
117
|
+
c.description = 'Show service logs'
|
118
|
+
c.option '-f', '--follow', 'Follow logs in real time'
|
119
|
+
c.action do |args, options|
|
120
|
+
Kontena::Cli::Services::Logs.new.show(args[0], options)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
command 'service stats' do |c|
|
125
|
+
c.syntax = 'kontena service stats <name>'
|
126
|
+
c.description = 'Show service stats'
|
127
|
+
c.option '-f', '--follow', 'Follow stats in real time'
|
128
|
+
c.action do |args, options|
|
129
|
+
Kontena::Cli::Services::Stats.new.show(args[0], options)
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require_relative '../common'
|
3
|
+
|
4
|
+
module Kontena::Cli::Services
|
5
|
+
class Containers
|
6
|
+
include Kontena::Cli::Common
|
7
|
+
|
8
|
+
##
|
9
|
+
# @param [String] service_id
|
10
|
+
def list(service_id)
|
11
|
+
require_api_url
|
12
|
+
token = require_token
|
13
|
+
|
14
|
+
result = client(token).get("services/#{service_id}/containers")
|
15
|
+
result['containers'].each do |container|
|
16
|
+
puts "#{container['id']}:"
|
17
|
+
puts " node: #{container['node']['name']}"
|
18
|
+
puts " ip (internal): #{container['network_settings']['ip_address']}"
|
19
|
+
puts " status: #{container['status']}"
|
20
|
+
puts ""
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require_relative '../common'
|
3
|
+
|
4
|
+
module Kontena::Cli::Services
|
5
|
+
class Logs
|
6
|
+
include Kontena::Cli::Common
|
7
|
+
|
8
|
+
##
|
9
|
+
# @param [String] service_id
|
10
|
+
def show(service_id, options)
|
11
|
+
require_api_url
|
12
|
+
token = require_token
|
13
|
+
last_id = nil
|
14
|
+
loop do
|
15
|
+
query_params = last_id.nil? ? '' : "from=#{last_id}"
|
16
|
+
result = client(token).get("services/#{service_id}/container_logs?#{query_params}")
|
17
|
+
result['logs'].each do |log|
|
18
|
+
puts log['data']
|
19
|
+
last_id = log['id']
|
20
|
+
end
|
21
|
+
break unless options.follow
|
22
|
+
sleep(2)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require_relative '../common'
|
3
|
+
|
4
|
+
module Kontena::Cli::Services
|
5
|
+
class Services
|
6
|
+
include Kontena::Cli::Common
|
7
|
+
|
8
|
+
def list
|
9
|
+
require_api_url
|
10
|
+
token = require_token
|
11
|
+
|
12
|
+
grids = client(token).get("grids/#{current_grid}/services")
|
13
|
+
puts "%-30.30s %-40.40s %-15s %-8s" % ['NAME', 'IMAGE', 'INSTANCES', 'STATE?']
|
14
|
+
grids['services'].each do |service|
|
15
|
+
state = service['stateful'] ? 'yes' : 'no'
|
16
|
+
puts "%-30.30s %-40.40s %-15.15s %-8s" % [service['id'], service['image'], service['container_count'], state]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def show(service_id)
|
21
|
+
require_api_url
|
22
|
+
token = require_token
|
23
|
+
|
24
|
+
service = client(token).get("services/#{service_id}")
|
25
|
+
puts "#{service['id']}:"
|
26
|
+
puts " status: #{service['state'] }"
|
27
|
+
puts " stateful: #{service['stateful'] == true ? 'yes' : 'no' }"
|
28
|
+
puts " scaling: #{service['container_count'] }"
|
29
|
+
puts " image: #{service['image']}"
|
30
|
+
if service['cmd']
|
31
|
+
puts " cmd: #{service['cmd'].join(' ')}"
|
32
|
+
else
|
33
|
+
puts " cmd: -"
|
34
|
+
end
|
35
|
+
|
36
|
+
puts " env: "
|
37
|
+
if service['env']
|
38
|
+
service['env'].each{|e| puts " - #{e}"}
|
39
|
+
end
|
40
|
+
puts " ports:"
|
41
|
+
service['ports'].each do |p|
|
42
|
+
puts " - #{p['node_port']}:#{p['container_port']}/#{p['protocol']}"
|
43
|
+
end
|
44
|
+
puts " links: "
|
45
|
+
if service['links']
|
46
|
+
service['links'].each do |p|
|
47
|
+
puts " - #{p['alias']}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
puts " containers:"
|
51
|
+
result = client(token).get("services/#{service_id}/containers")
|
52
|
+
result['containers'].each do |container|
|
53
|
+
puts " #{container['id']}:"
|
54
|
+
puts " rev: #{container['deploy_rev']}"
|
55
|
+
puts " node: #{container['node']['name']}"
|
56
|
+
puts " dns: #{container['id']}.kontena.local"
|
57
|
+
puts " ip: #{container['network_settings']['ip_address']}"
|
58
|
+
if container['status'] == 'unknown'
|
59
|
+
puts " status: #{container['status'].colorize(:yellow)}"
|
60
|
+
else
|
61
|
+
puts " status: #{container['status']}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def scale(service_id, count)
|
67
|
+
client(require_token).put("services/#{service_id}", {container_count: count})
|
68
|
+
self.deploy(service_id)
|
69
|
+
end
|
70
|
+
|
71
|
+
def deploy(service_id)
|
72
|
+
require_api_url
|
73
|
+
token = require_token
|
74
|
+
|
75
|
+
result = client(token).post("services/#{service_id}/deploy", {})
|
76
|
+
|
77
|
+
print 'deploying '
|
78
|
+
until client(token).get("services/#{service_id}")['state'] != 'deploying' do
|
79
|
+
print '.'
|
80
|
+
sleep 1
|
81
|
+
end
|
82
|
+
puts ' done'
|
83
|
+
puts ''
|
84
|
+
self.show(service_id)
|
85
|
+
end
|
86
|
+
|
87
|
+
def restart(service_id)
|
88
|
+
require_api_url
|
89
|
+
token = require_token
|
90
|
+
|
91
|
+
result = client(token).post("services/#{service_id}/restart", {})
|
92
|
+
end
|
93
|
+
|
94
|
+
def stop(service_id)
|
95
|
+
require_api_url
|
96
|
+
token = require_token
|
97
|
+
|
98
|
+
result = client(token).post("services/#{service_id}/stop", {})
|
99
|
+
end
|
100
|
+
|
101
|
+
def start(service_id)
|
102
|
+
require_api_url
|
103
|
+
token = require_token
|
104
|
+
|
105
|
+
result = client(token).post("services/#{service_id}/start", {})
|
106
|
+
end
|
107
|
+
|
108
|
+
def create(name, image, options)
|
109
|
+
require_api_url
|
110
|
+
token = require_token
|
111
|
+
if options.ports
|
112
|
+
ports = parse_ports(options.ports)
|
113
|
+
end
|
114
|
+
data = {
|
115
|
+
name: name,
|
116
|
+
image: image,
|
117
|
+
stateful: !!options.stateful
|
118
|
+
}
|
119
|
+
if options.link
|
120
|
+
links = parse_links(options.link)
|
121
|
+
end
|
122
|
+
data[:ports] = ports if options.ports
|
123
|
+
data[:links] = links if options.link
|
124
|
+
data[:volumes] = options.volume if options.volume
|
125
|
+
data[:volumes_from] = options.volumes_from if options.volumes_from
|
126
|
+
data[:memory] = parse_memory(options.memory) if options.memory
|
127
|
+
data[:memory_swap] = parse_memory(options.memory_swap) if options.memory_swap
|
128
|
+
data[:cpu_shares] = options.cpu_shares if options.cpu_shares
|
129
|
+
data[:affinity] = options.affinity if options.affinity
|
130
|
+
data[:env] = options.env if options.env
|
131
|
+
data[:container_count] = options.instances if options.instances
|
132
|
+
data[:cmd] = options.cmd.split(" ") if options.cmd
|
133
|
+
data[:user] = options.user if options.user
|
134
|
+
data[:cpu] = options.cpu if options.cpu
|
135
|
+
if options.memory
|
136
|
+
memory = human_size_to_number(options.memory)
|
137
|
+
raise ArgumentError.new('Invalid --memory')
|
138
|
+
data[:memory] = memory
|
139
|
+
end
|
140
|
+
data[:memory] = options.memory if options.memory
|
141
|
+
client(token).post("grids/#{current_grid}/services", data)
|
142
|
+
end
|
143
|
+
|
144
|
+
def update(service_id, options)
|
145
|
+
require_api_url
|
146
|
+
token = require_token
|
147
|
+
|
148
|
+
data = {}
|
149
|
+
data[:env] = options.env if options.env
|
150
|
+
data[:container_count] = options.instances if options.instances
|
151
|
+
data[:cmd] = options.cmd.split(" ") if options.cmd
|
152
|
+
data[:ports] = parse_ports(options.ports) if options.ports
|
153
|
+
data[:image] = options.image if options.image
|
154
|
+
|
155
|
+
client(require_token).put("services/#{service_id}", data)
|
156
|
+
end
|
157
|
+
|
158
|
+
def destroy(service_id)
|
159
|
+
require_api_url
|
160
|
+
token = require_token
|
161
|
+
|
162
|
+
result = client(token).delete("services/#{service_id}")
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
def current_grid
|
167
|
+
inifile['server']['grid']
|
168
|
+
end
|
169
|
+
|
170
|
+
def parse_ports(port_options)
|
171
|
+
port_options.map{|p|
|
172
|
+
node_port, container_port = p.split(':')
|
173
|
+
if node_port.nil? || container_port.nil?
|
174
|
+
raise ArgumentError.new("Invalid port value #{p}")
|
175
|
+
end
|
176
|
+
{
|
177
|
+
container_port: container_port,
|
178
|
+
node_port: node_port
|
179
|
+
}
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_links(link_options)
|
184
|
+
link_options.map{|l|
|
185
|
+
service_name, alias_name = l.split(':')
|
186
|
+
if service_name.nil? || alias_name.nil?
|
187
|
+
raise ArgumentError.new("Invalid link value #{l}")
|
188
|
+
end
|
189
|
+
{
|
190
|
+
name: service_name,
|
191
|
+
alias: alias_name
|
192
|
+
}
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|