kontena-cli 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/Dockerfile +16 -0
- data/Gemfile +3 -0
- data/Rakefile +4 -0
- data/kontena-docker.sh +6 -0
- data/lib/kontena/cli/commands.rb +2 -1
- data/lib/kontena/cli/common.rb +4 -0
- data/lib/kontena/cli/grids/commands.rb +28 -0
- data/lib/kontena/cli/grids/vpn.rb +71 -0
- data/lib/kontena/cli/nodes/nodes.rb +1 -0
- data/lib/kontena/cli/server/user.rb +11 -1
- data/lib/kontena/cli/services/logs.rb +20 -2
- data/lib/kontena/cli/services/services.rb +34 -78
- data/lib/kontena/cli/services/services_helper.rb +77 -0
- data/lib/kontena/cli/services/stats.rb +0 -14
- data/lib/kontena/cli/stacks/commands.rb +12 -0
- data/lib/kontena/cli/stacks/stacks.rb +148 -0
- data/lib/kontena/cli/version.rb +1 -1
- data/lib/kontena/client.rb +5 -1
- data/spec/kontena/cli/services/services_helper_spec.rb +103 -0
- data/spec/kontena/cli/stacks/stacks_spec.rb +179 -0
- data/spec/spec_helper.rb +19 -0
- data/tasks/rspec.rake +5 -0
- metadata +16 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7da42d25b33946f8887d78d4c1e966ddce0836d4
|
4
|
+
data.tar.gz: 9cc5dbbd4eae20285b82d9c06a790560deface6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3375614c89cd186046754ae837082edb85c894dabf54ec97ac35a45281f16c173490889ac814bf503e8211baf904b358717707a377f15702e0356bc05388c30e
|
7
|
+
data.tar.gz: 967ee20ab75a338ae4a6ca872b193a3fe8087d79c1c20d56a6f9b72cab1f0fa80a131af6f84e34992fe64ddb3b88277345fa05b1a586e0ec65632253bdc1c404
|
data/Dockerfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
FROM gliderlabs/alpine:edge
|
2
|
+
MAINTAINER jari@kontena.io
|
3
|
+
|
4
|
+
RUN apk update && \
|
5
|
+
apk --update add ruby ruby-json ca-certificates libssl1.0 openssl libstdc++ && \
|
6
|
+
gem install kontena-cli --no-rdoc --no-ri
|
7
|
+
|
8
|
+
|
9
|
+
RUN adduser kontena -D -h /home/kontena -s /bin/sh
|
10
|
+
RUN chown -R kontena.kontena /home/kontena
|
11
|
+
|
12
|
+
|
13
|
+
VOLUME ["/home/kontena"]
|
14
|
+
WORKDIR /home/kontena
|
15
|
+
USER kontena
|
16
|
+
ENTRYPOINT ["/usr/bin/kontena"]
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/kontena-docker.sh
ADDED
data/lib/kontena/cli/commands.rb
CHANGED
@@ -16,4 +16,5 @@ require_relative 'server/commands'
|
|
16
16
|
require_relative 'containers/commands'
|
17
17
|
require_relative 'grids/commands'
|
18
18
|
require_relative 'nodes/commands'
|
19
|
-
require_relative 'services/commands'
|
19
|
+
require_relative 'services/commands'
|
20
|
+
require_relative 'stacks/commands'
|
data/lib/kontena/cli/common.rb
CHANGED
@@ -3,6 +3,7 @@ module Kontena::Cli::Grids; end;
|
|
3
3
|
require_relative 'grids'
|
4
4
|
require_relative 'users'
|
5
5
|
require_relative 'audit_log'
|
6
|
+
require_relative 'vpn'
|
6
7
|
|
7
8
|
|
8
9
|
command 'grid list' do |c|
|
@@ -87,3 +88,30 @@ command 'grid remove-user' do |c|
|
|
87
88
|
Kontena::Cli::Grids::Users.new.remove(args[0])
|
88
89
|
end
|
89
90
|
end
|
91
|
+
|
92
|
+
|
93
|
+
command 'vpn create' do |c|
|
94
|
+
c.syntax = 'kontena vpn create'
|
95
|
+
c.description = 'Create vpn service'
|
96
|
+
c.option '--node STRING', String, 'Node name'
|
97
|
+
c.option '--ip STRING', String, 'Node ip'
|
98
|
+
c.action do |args, options|
|
99
|
+
Kontena::Cli::Grids::Vpn.new.create(options)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
command 'vpn delete' do |c|
|
104
|
+
c.syntax = 'kontena vpn delete'
|
105
|
+
c.description = 'Delete vpn service'
|
106
|
+
c.action do |args, options|
|
107
|
+
Kontena::Cli::Grids::Vpn.new.delete
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
command 'vpn config' do |c|
|
112
|
+
c.syntax = 'kontena vpn config'
|
113
|
+
c.description = 'Show vpn client config'
|
114
|
+
c.action do |args, options|
|
115
|
+
Kontena::Cli::Grids::Vpn.new.config
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require_relative '../common'
|
3
|
+
|
4
|
+
module Kontena::Cli::Grids
|
5
|
+
class Vpn
|
6
|
+
include Kontena::Cli::Common
|
7
|
+
|
8
|
+
def create(opts)
|
9
|
+
require_api_url
|
10
|
+
token = require_token
|
11
|
+
preferred_node = opts.node
|
12
|
+
|
13
|
+
vpn = client(token).get("services/vpn") rescue nil
|
14
|
+
raise ArgumentError.new('Vpn already exists') if vpn
|
15
|
+
|
16
|
+
nodes = client(token).get("grids/#{current_grid}/nodes")
|
17
|
+
if preferred_node.nil?
|
18
|
+
node = nodes['nodes'].find{|n| n['connected']}
|
19
|
+
raise ArgumentError.new('Cannot find any online nodes') if node.nil?
|
20
|
+
else
|
21
|
+
node = nodes['nodes'].find{|n| n['connected'] && n['name'] == preferred_node }
|
22
|
+
raise ArgumentError.new('Node not found') if node.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
public_ip = opts.ip || node['public_ip']
|
26
|
+
|
27
|
+
data = {
|
28
|
+
name: 'vpn',
|
29
|
+
stateful: true,
|
30
|
+
image: 'kontena/openvpn:latest',
|
31
|
+
ports: [
|
32
|
+
{
|
33
|
+
container_port: '1194',
|
34
|
+
node_port: '1194',
|
35
|
+
protocol: 'udp'
|
36
|
+
}
|
37
|
+
],
|
38
|
+
cap_add: ['NET_ADMIN'],
|
39
|
+
env: ["OVPN_SERVER_URL=udp://#{public_ip}:1194"],
|
40
|
+
affinity: ["node==#{node['name']}"]
|
41
|
+
}
|
42
|
+
client(token).post("grids/#{current_grid}/services", data)
|
43
|
+
result = client(token).post("services/vpn/deploy", {})
|
44
|
+
print 'deploying '
|
45
|
+
until client(token).get("services/vpn")['state'] != 'deploying' do
|
46
|
+
print '.'
|
47
|
+
sleep 1
|
48
|
+
end
|
49
|
+
puts ' done'
|
50
|
+
puts "OpenVPN service is now started (udp://#{public_ip}:1194)."
|
51
|
+
puts "Use 'kontena vpn config' to fetch OpenVPN client config to your machine (it takes a while until config is ready)."
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete
|
55
|
+
require_api_url
|
56
|
+
token = require_token
|
57
|
+
|
58
|
+
vpn = client(token).get("services/vpn") rescue nil
|
59
|
+
raise ArgumentError.new("VPN service does not exist") if vpn.nil?
|
60
|
+
|
61
|
+
client(token).delete("services/vpn")
|
62
|
+
end
|
63
|
+
|
64
|
+
def config
|
65
|
+
require_api_url
|
66
|
+
payload = {cmd: ['/usr/local/bin/ovpn_getclient', 'KONTENA_VPN_CLIENT']}
|
67
|
+
stdout, stderr = client(require_token).post("containers/vpn-1/exec", payload)
|
68
|
+
puts stdout
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -38,6 +38,7 @@ module Kontena::Cli::Nodes
|
|
38
38
|
puts " id: #{node['id']}"
|
39
39
|
puts " connected: #{node['connected'] ? 'yes': 'no'}"
|
40
40
|
puts " last connect: #{node['updated_at']}"
|
41
|
+
puts " public ip: #{node['public_ip']}"
|
41
42
|
puts " os: #{node['os']}"
|
42
43
|
puts " driver: #{node['driver']}"
|
43
44
|
puts " kernel: #{node['kernel_version']}"
|
@@ -21,7 +21,17 @@ module Kontena::Cli::Server
|
|
21
21
|
if response
|
22
22
|
settings['server']['token'] = response['access_token']
|
23
23
|
save_settings
|
24
|
-
|
24
|
+
puts ''
|
25
|
+
puts "Welcome #{response['user']['name'].green}"
|
26
|
+
puts ''
|
27
|
+
reset_client
|
28
|
+
grid = client(require_token).get('grids')['grids'][0]
|
29
|
+
if grid
|
30
|
+
self.current_grid = grid
|
31
|
+
puts "Using grid: #{grid['name'].cyan}"
|
32
|
+
else
|
33
|
+
clear_current_grid
|
34
|
+
end
|
25
35
|
true
|
26
36
|
else
|
27
37
|
print color('Login Failed', :red)
|
@@ -15,12 +15,30 @@ module Kontena::Cli::Services
|
|
15
15
|
query_params = last_id.nil? ? '' : "from=#{last_id}"
|
16
16
|
result = client(token).get("services/#{service_id}/container_logs?#{query_params}")
|
17
17
|
result['logs'].each do |log|
|
18
|
-
|
18
|
+
color = color_for_container(log['container_id'])
|
19
|
+
puts "#{log['container_id'][0..12].colorize(color)} | #{log['data']}"
|
19
20
|
last_id = log['id']
|
20
21
|
end
|
21
22
|
break unless options.follow
|
22
23
|
sleep(2)
|
23
24
|
end
|
24
25
|
end
|
26
|
+
|
27
|
+
def color_for_container(container_id)
|
28
|
+
color_maps[container_id] = colors.shift unless color_maps[container_id]
|
29
|
+
color_maps[container_id].to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def color_maps
|
33
|
+
@color_maps ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def colors
|
37
|
+
if(@colors.nil? || @colors.size == 0)
|
38
|
+
@colors = [:green, :yellow, :magenta, :cyan, :red,
|
39
|
+
:light_green, :light_yellow, :ligh_magenta, :light_cyan, :light_red]
|
40
|
+
end
|
41
|
+
@colors
|
42
|
+
end
|
25
43
|
end
|
26
|
-
end
|
44
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'kontena/client'
|
2
2
|
require_relative '../common'
|
3
|
+
require_relative 'services_helper'
|
3
4
|
|
4
5
|
module Kontena::Cli::Services
|
5
6
|
class Services
|
6
7
|
include Kontena::Cli::Common
|
8
|
+
include Kontena::Cli::Services::ServicesHelper
|
7
9
|
|
8
10
|
def list
|
9
11
|
require_api_url
|
@@ -21,7 +23,7 @@ module Kontena::Cli::Services
|
|
21
23
|
require_api_url
|
22
24
|
token = require_token
|
23
25
|
|
24
|
-
service =
|
26
|
+
service = get_service(token, service_id)
|
25
27
|
puts "#{service['id']}:"
|
26
28
|
puts " status: #{service['state'] }"
|
27
29
|
puts " stateful: #{service['stateful'] == true ? 'yes' : 'no' }"
|
@@ -43,8 +45,8 @@ module Kontena::Cli::Services
|
|
43
45
|
end
|
44
46
|
puts " links: "
|
45
47
|
if service['links']
|
46
|
-
service['links'].each do |
|
47
|
-
puts " - #{
|
48
|
+
service['links'].each do |l|
|
49
|
+
puts " - #{l['alias']}"
|
48
50
|
end
|
49
51
|
end
|
50
52
|
puts " containers:"
|
@@ -55,6 +57,7 @@ module Kontena::Cli::Services
|
|
55
57
|
puts " node: #{container['node']['name']}"
|
56
58
|
puts " dns: #{container['id']}.kontena.local"
|
57
59
|
puts " ip: #{container['network_settings']['ip_address']}"
|
60
|
+
puts " public ip: #{container['node']['public_ip']}"
|
58
61
|
if container['status'] == 'unknown'
|
59
62
|
puts " status: #{container['status'].colorize(:yellow)}"
|
60
63
|
else
|
@@ -71,33 +74,23 @@ module Kontena::Cli::Services
|
|
71
74
|
def deploy(service_id, options)
|
72
75
|
require_api_url
|
73
76
|
token = require_token
|
74
|
-
|
75
77
|
data = {}
|
76
78
|
data[:strategy] = options.strategy if options.strategy
|
77
79
|
data[:wait_for_port] = options.wait_for_port if options.wait_for_port
|
78
|
-
|
79
|
-
|
80
|
-
print 'deploying '
|
81
|
-
until client(token).get("services/#{service_id}")['state'] != 'deploying' do
|
82
|
-
print '.'
|
83
|
-
sleep 1
|
84
|
-
end
|
85
|
-
puts ' done'
|
86
|
-
puts ''
|
80
|
+
deploy_service(token, service_id, data)
|
87
81
|
self.show(service_id)
|
88
82
|
end
|
89
83
|
|
84
|
+
|
90
85
|
def restart(service_id)
|
91
86
|
require_api_url
|
92
87
|
token = require_token
|
93
|
-
|
94
88
|
result = client(token).post("services/#{service_id}/restart", {})
|
95
89
|
end
|
96
90
|
|
97
91
|
def stop(service_id)
|
98
92
|
require_api_url
|
99
93
|
token = require_token
|
100
|
-
|
101
94
|
result = client(token).post("services/#{service_id}/stop", {})
|
102
95
|
end
|
103
96
|
|
@@ -111,55 +104,22 @@ module Kontena::Cli::Services
|
|
111
104
|
def create(name, image, options)
|
112
105
|
require_api_url
|
113
106
|
token = require_token
|
114
|
-
if options.ports
|
115
|
-
ports = parse_ports(options.ports)
|
116
|
-
end
|
117
107
|
data = {
|
118
108
|
name: name,
|
119
109
|
image: image,
|
120
110
|
stateful: !!options.stateful
|
121
111
|
}
|
122
|
-
|
123
|
-
|
124
|
-
end
|
125
|
-
data[:ports] = ports if options.ports
|
126
|
-
data[:links] = links if options.link
|
127
|
-
data[:volumes] = options.volume if options.volume
|
128
|
-
data[:volumes_from] = options.volumes_from if options.volumes_from
|
129
|
-
data[:memory] = parse_memory(options.memory) if options.memory
|
130
|
-
data[:memory_swap] = parse_memory(options.memory_swap) if options.memory_swap
|
131
|
-
data[:cpu_shares] = options.cpu_shares if options.cpu_shares
|
132
|
-
data[:affinity] = options.affinity if options.affinity
|
133
|
-
data[:env] = options.env if options.env
|
134
|
-
data[:container_count] = options.instances if options.instances
|
135
|
-
data[:cmd] = options.cmd.split(" ") if options.cmd
|
136
|
-
data[:user] = options.user if options.user
|
137
|
-
data[:cpu] = options.cpu if options.cpu
|
138
|
-
data[:cap_add] = options.cap_add if options.cap_add
|
139
|
-
data[:cap_drop] = options.cap_drop if options.cap_drop
|
140
|
-
if options.memory
|
141
|
-
memory = human_size_to_number(options.memory)
|
142
|
-
raise ArgumentError.new('Invalid --memory')
|
143
|
-
data[:memory] = memory
|
144
|
-
end
|
145
|
-
data[:memory] = options.memory if options.memory
|
146
|
-
client(token).post("grids/#{current_grid}/services", data)
|
112
|
+
data.merge!(parse_data_from_options(options))
|
113
|
+
create_service(token, current_grid, data)
|
147
114
|
end
|
148
115
|
|
116
|
+
|
149
117
|
def update(service_id, options)
|
150
118
|
require_api_url
|
151
119
|
token = require_token
|
152
120
|
|
153
|
-
data =
|
154
|
-
|
155
|
-
data[:container_count] = options.instances if options.instances
|
156
|
-
data[:cmd] = options.cmd.split(" ") if options.cmd
|
157
|
-
data[:ports] = parse_ports(options.ports) if options.ports
|
158
|
-
data[:image] = options.image if options.image
|
159
|
-
data[:cap_add] = options.cap_add if options.cap_add
|
160
|
-
data[:cap_drop] = options.cap_drop if options.cap_drop
|
161
|
-
|
162
|
-
client(require_token).put("services/#{service_id}", data)
|
121
|
+
data = parse_data_from_options(options)
|
122
|
+
update_service(token, service_id, data)
|
163
123
|
end
|
164
124
|
|
165
125
|
def destroy(service_id)
|
@@ -171,31 +131,27 @@ module Kontena::Cli::Services
|
|
171
131
|
|
172
132
|
private
|
173
133
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
name: service_name,
|
196
|
-
alias: alias_name
|
197
|
-
}
|
198
|
-
}
|
134
|
+
##
|
135
|
+
# parse given options to hash
|
136
|
+
# @return [Hash]
|
137
|
+
def parse_data_from_options(options)
|
138
|
+
data = {}
|
139
|
+
data[:ports] = parse_ports(options.ports) if options.ports
|
140
|
+
data[:links] = parse_links(options.link) if options.link
|
141
|
+
data[:volumes] = options.volume if options.volume
|
142
|
+
data[:volumes_from] = options.volumes_from if options.volumes_from
|
143
|
+
data[:memory] = parse_memory(options.memory) if options.memory
|
144
|
+
data[:memory_swap] = parse_memory(options.memory_swap) if options.memory_swap
|
145
|
+
data[:cpu_shares] = options.cpu_shares if options.cpu_shares
|
146
|
+
data[:affinity] = options.affinity if options.affinity
|
147
|
+
data[:env] = options.env if options.env
|
148
|
+
data[:container_count] = options.instances if options.instances
|
149
|
+
data[:cmd] = options.cmd.split(" ") if options.cmd
|
150
|
+
data[:user] = options.user if options.user
|
151
|
+
data[:image] = options.image if options.image
|
152
|
+
data[:cap_add] = options.cap_add if options.cap_add
|
153
|
+
data[:cap_drop] = options.cap_drop if options.cap_drop
|
154
|
+
data
|
199
155
|
end
|
200
156
|
end
|
201
157
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require_relative '../common'
|
3
|
+
|
4
|
+
module Kontena
|
5
|
+
module Cli
|
6
|
+
module Services
|
7
|
+
module ServicesHelper
|
8
|
+
include Kontena::Cli::Common
|
9
|
+
|
10
|
+
def create_service(token, grid_id, data)
|
11
|
+
client(token).post("grids/#{grid_id}/services", data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_service(token, service_id, data)
|
15
|
+
client(token).put("services/#{service_id}", data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_service(token, service_id)
|
19
|
+
client(token).get("services/#{service_id}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def deploy_service(token, service_id, data)
|
23
|
+
client(token).post("services/#{service_id}/deploy", data)
|
24
|
+
print 'deploying '
|
25
|
+
until client(token).get("services/#{service_id}")['state'] != 'deploying' do
|
26
|
+
print '.'
|
27
|
+
sleep 1
|
28
|
+
end
|
29
|
+
puts ' done'
|
30
|
+
puts ''
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_ports(port_options)
|
34
|
+
port_options.map{|p|
|
35
|
+
node_port, container_port, protocol = p.split(':')
|
36
|
+
if node_port.nil? || container_port.nil?
|
37
|
+
raise ArgumentError.new("Invalid port value #{p}")
|
38
|
+
end
|
39
|
+
{
|
40
|
+
container_port: container_port,
|
41
|
+
node_port: node_port,
|
42
|
+
protocol: protocol || 'tcp'
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_links(link_options)
|
48
|
+
link_options.map{|l|
|
49
|
+
service_name, alias_name = l.split(':')
|
50
|
+
if service_name.nil?
|
51
|
+
raise ArgumentError.new("Invalid link value #{l}")
|
52
|
+
end
|
53
|
+
alias_name = service_name if alias_name.nil?
|
54
|
+
{
|
55
|
+
name: service_name,
|
56
|
+
alias: alias_name
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_memory(memory)
|
62
|
+
if memory.end_with?('k')
|
63
|
+
memory.to_i * 1000
|
64
|
+
elsif memory.end_with?('m')
|
65
|
+
memory.to_i * 1000000
|
66
|
+
elsif memory.end_with?('g')
|
67
|
+
memory.to_i * 1000000000
|
68
|
+
else
|
69
|
+
memory.to_i
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -51,20 +51,6 @@ module Kontena::Cli::Services
|
|
51
51
|
network_out = stat['network'].nil? ? 'N/A' : filesize_to_human(stat['network']['tx_bytes'])
|
52
52
|
puts '%-30.30s %-15s %-20s %-15s %-15s' % [ stat['container_id'], "#{cpu}%", "#{memory} / #{memory_limit}", "#{memory_pct}", "#{network_in}/#{network_out}"]
|
53
53
|
end
|
54
|
-
##
|
55
|
-
# @param [String] memory
|
56
|
-
# @return [Integer]
|
57
|
-
def parse_memory(memory)
|
58
|
-
if memory.end_with?('k')
|
59
|
-
memory.to_i * 1000
|
60
|
-
elsif memory.end_with?('m')
|
61
|
-
memory.to_i * 1000000
|
62
|
-
elsif memory.end_with?('g')
|
63
|
-
memory.to_i * 1000000000
|
64
|
-
else
|
65
|
-
memory.to_i
|
66
|
-
end
|
67
|
-
end
|
68
54
|
|
69
55
|
##
|
70
56
|
# @param [Integer] size
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Kontena::Cli::Stacks; end;
|
2
|
+
require_relative 'stacks'
|
3
|
+
|
4
|
+
command 'deploy' do |c|
|
5
|
+
c.syntax = 'kontena deploy'
|
6
|
+
c.description = 'Create and deploy multiple services from YAML file'
|
7
|
+
c.option '-f', '--file String', 'path to kontena.yml file, default: current directory'
|
8
|
+
c.option '-p', '--prefix String', 'prefix of service names, default: name of the current directory'
|
9
|
+
c.action do |args, options|
|
10
|
+
Kontena::Cli::Stacks::Stacks.new.deploy(options)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'kontena/client'
|
2
|
+
require 'yaml'
|
3
|
+
require_relative '../common'
|
4
|
+
require_relative '../services/services_helper'
|
5
|
+
|
6
|
+
module Kontena::Cli::Stacks
|
7
|
+
class Stacks
|
8
|
+
include Kontena::Cli::Common
|
9
|
+
include Kontena::Cli::Services::ServicesHelper
|
10
|
+
|
11
|
+
attr_reader :services, :service_prefix, :deploy_queue
|
12
|
+
def initialize
|
13
|
+
@deploy_queue = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def deploy(options)
|
17
|
+
require_api_url
|
18
|
+
require_token
|
19
|
+
|
20
|
+
filename = options.file || './kontena.yml'
|
21
|
+
@services = YAML.load(File.read(filename))
|
22
|
+
@service_prefix = options.prefix || current_dir
|
23
|
+
|
24
|
+
Dir.chdir(File.dirname(filename))
|
25
|
+
init_services(services)
|
26
|
+
deploy_services(deploy_queue)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def init_services(services)
|
32
|
+
services.each do |name, config|
|
33
|
+
create_or_update_service(prefixed_name(name), config)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def deploy_services(queue)
|
38
|
+
queue.each do |service|
|
39
|
+
puts "deploying #{service['id']}"
|
40
|
+
data = {}
|
41
|
+
if service['deploy']
|
42
|
+
data[:strategy] = service['deploy']['strategy'] if service['deploy']['strategy']
|
43
|
+
data[:wait_for_port] = service['deploy']['wait_for_port'] if service['deploy']['wait_for_port']
|
44
|
+
end
|
45
|
+
deploy_service(token, service['id'], data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_or_update_service(name, options)
|
50
|
+
# skip if service is already created or updated
|
51
|
+
return nil if in_deploy_queue?(name)
|
52
|
+
|
53
|
+
# create/update linked services recursively before continuing
|
54
|
+
unless options['links'].nil?
|
55
|
+
parse_links(options['links']).each_with_index do |linked_service, index|
|
56
|
+
# change prefixed service name also to links options
|
57
|
+
options['links'][index] = "#{prefixed_name(linked_service[:name])}:#{linked_service[:alias]}"
|
58
|
+
|
59
|
+
create_or_update_service(prefixed_name(linked_service[:name]), services[linked_service[:name]]) unless in_deploy_queue?(prefixed_name(linked_service[:name]))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
merge_env_vars(options)
|
64
|
+
|
65
|
+
if find_service_by_name(name)
|
66
|
+
service = update(name, options)
|
67
|
+
else
|
68
|
+
service = create(name, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
# add deploy options to service
|
72
|
+
service['deploy'] = options['deploy']
|
73
|
+
|
74
|
+
deploy_queue.push service
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_service_by_name(name)
|
78
|
+
get_service(token, name) rescue nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def create(name, options)
|
82
|
+
puts "creating #{name}"
|
83
|
+
data = {name: name}
|
84
|
+
data.merge!(parse_data(options))
|
85
|
+
create_service(token, current_grid, data)
|
86
|
+
end
|
87
|
+
|
88
|
+
def update(id, options)
|
89
|
+
data = parse_data(options)
|
90
|
+
puts "updating #{id}"
|
91
|
+
update_service(token, id, data)
|
92
|
+
end
|
93
|
+
|
94
|
+
def in_deploy_queue?(name)
|
95
|
+
deploy_queue.find {|service| service['id'] == name} != nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def prefixed_name(name)
|
99
|
+
"#{service_prefix}-#{name}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def current_dir
|
103
|
+
File.basename(Dir.getwd)
|
104
|
+
end
|
105
|
+
|
106
|
+
def merge_env_vars(options)
|
107
|
+
return unless options['env_file']
|
108
|
+
|
109
|
+
options['env_file'] = [options['env_file']] if options['env_file'].is_a?(String)
|
110
|
+
options['environment'] = [] unless options['environment']
|
111
|
+
|
112
|
+
options['env_file'].each do |env_file|
|
113
|
+
options['environment'].concat(read_env_file(env_file))
|
114
|
+
end
|
115
|
+
|
116
|
+
options['environment'].uniq! {|s| s.split('=').first}
|
117
|
+
end
|
118
|
+
|
119
|
+
def read_env_file(path)
|
120
|
+
File.readlines(path).delete_if { |line| line.start_with?('#') || line.empty? }
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_data(options)
|
124
|
+
data = {}
|
125
|
+
data[:image] = options['image']
|
126
|
+
data[:env] = options['environment']
|
127
|
+
data[:container_count] = options['instances']
|
128
|
+
data[:links] = parse_links(options['links']) if options['links']
|
129
|
+
data[:ports] = parse_ports(options['ports']) if options['ports']
|
130
|
+
data[:memory] = parse_memory(options['mem_limit']) if options['mem_limit']
|
131
|
+
data[:memory_swap] = parse_memory(options['memswap_limit']) if options['memswap_limit']
|
132
|
+
data[:cpu_shares] = options['cpu_shares'] if options['cpu_shares']
|
133
|
+
data[:volumes] = options['volume'] if options['volume']
|
134
|
+
data[:volumes_from] = options['volumes_from'] if options['volumes_from']
|
135
|
+
data[:cmd] = options['command'].split(" ") if options['command']
|
136
|
+
data[:affinity] = options['affinity'] if options['affinity']
|
137
|
+
data[:user] = options['user'] if options['user']
|
138
|
+
data[:stateful] = options['stateful'] == true
|
139
|
+
data[:cap_add] = options['cap_add'] if options['cap_add']
|
140
|
+
data[:cap_drop] = options['cap_drop'] if options['cap_drop']
|
141
|
+
data
|
142
|
+
end
|
143
|
+
|
144
|
+
def token
|
145
|
+
@token ||= require_token
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/kontena/cli/version.rb
CHANGED
data/lib/kontena/client.rb
CHANGED
@@ -171,7 +171,11 @@ module Kontena
|
|
171
171
|
end
|
172
172
|
|
173
173
|
def handle_error_response(response)
|
174
|
-
|
174
|
+
message = response.body
|
175
|
+
if response.status == 404 && message == ''
|
176
|
+
message = 'Not found'
|
177
|
+
end
|
178
|
+
raise Kontena::Errors::StandardError.new(response.status, message)
|
175
179
|
end
|
176
180
|
end
|
177
181
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative "../../../spec_helper"
|
2
|
+
require "kontena/cli/services/services_helper"
|
3
|
+
|
4
|
+
module Kontena::Cli::Services
|
5
|
+
describe ServicesHelper do
|
6
|
+
subject{klass.new}
|
7
|
+
|
8
|
+
let(:klass) { Class.new { include ServicesHelper } }
|
9
|
+
|
10
|
+
let(:client) do
|
11
|
+
double
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:token) do
|
15
|
+
'token'
|
16
|
+
end
|
17
|
+
|
18
|
+
before(:each) do
|
19
|
+
allow(subject).to receive(:client).with(token).and_return(client)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#create_service' do
|
23
|
+
it 'creates POST grids/:id/services request to Kontena Server' do
|
24
|
+
expect(client).to receive(:post).with('grids/1/services', {'name' => 'test-service'})
|
25
|
+
subject.create_service(token, '1', {'name' => 'test-service'})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#update_service' do
|
30
|
+
it 'creates PUT services/:id request to Kontena Server' do
|
31
|
+
expect(client).to receive(:put).with('services/1', {'name' => 'test-service'})
|
32
|
+
subject.update_service(token, '1', {'name' => 'test-service'})
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#get_service' do
|
37
|
+
it 'creates GET services/:id request to Kontena Server' do
|
38
|
+
expect(client).to receive(:get).with('services/test-service')
|
39
|
+
subject.get_service(token, 'test-service')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#deploy_service' do
|
44
|
+
it 'creates POST services/:id/deploy request to Kontena Server' do
|
45
|
+
allow(client).to receive(:get).with('services/1').and_return({'state' => 'running'})
|
46
|
+
expect(client).to receive(:post).with('services/1/deploy', {'strategy' => 'ha'})
|
47
|
+
subject.deploy_service(token, '1', {'strategy' => 'ha'})
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'polls Kontena Server until service is running' do
|
51
|
+
allow(client).to receive(:post).with('services/1/deploy', anything)
|
52
|
+
expect(client).to receive(:get).with('services/1').twice.and_return({'state' => 'deploying'}, {'state' => 'running'})
|
53
|
+
|
54
|
+
subject.deploy_service(token, '1', {'strategy' => 'ha'})
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#parse_ports' do
|
59
|
+
it 'raises error if node_port is missing' do
|
60
|
+
expect{
|
61
|
+
subject.parse_ports(["80"])
|
62
|
+
}.to raise_error(ArgumentError)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'raises error if container_port is missing' do
|
66
|
+
expect{
|
67
|
+
subject.parse_ports(["80:"])
|
68
|
+
}.to raise_error(ArgumentError)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns hash of port options' do
|
72
|
+
valid_result = [{
|
73
|
+
container_port: '80',
|
74
|
+
node_port: '80',
|
75
|
+
protocol: 'tcp'
|
76
|
+
}]
|
77
|
+
port_options = subject.parse_ports(['80:80'])
|
78
|
+
|
79
|
+
expect(port_options).to eq(valid_result)
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#parse_links' do
|
85
|
+
it 'raises error if service name is missing' do
|
86
|
+
expect{
|
87
|
+
subject.parse_links([""])
|
88
|
+
}.to raise_error(ArgumentError)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns hash of link options' do
|
92
|
+
valid_result = [{
|
93
|
+
name: 'db',
|
94
|
+
alias: 'mysql',
|
95
|
+
}]
|
96
|
+
link_options = subject.parse_links(['db:mysql'])
|
97
|
+
|
98
|
+
expect(link_options).to eq(valid_result)
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require_relative "../../../spec_helper"
|
2
|
+
require "kontena/cli/stacks/stacks"
|
3
|
+
|
4
|
+
module Kontena::Cli::Stacks
|
5
|
+
describe Stacks do
|
6
|
+
let(:settings) do
|
7
|
+
{'server' => {'url' => 'http://kontena.test', 'token' => token}}
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:token) do
|
11
|
+
'1234567'
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
let(:services) do
|
16
|
+
{
|
17
|
+
'wordpress' => {
|
18
|
+
'image' => 'wordpress:latest',
|
19
|
+
'links' => ['mysql:db'],
|
20
|
+
'ports' => ['80:80'],
|
21
|
+
'instances' => 2,
|
22
|
+
'deploy' => {
|
23
|
+
'strategy' => 'ha'
|
24
|
+
}
|
25
|
+
},
|
26
|
+
'mysql' => {
|
27
|
+
'image' => 'mysql:5.6',
|
28
|
+
'stateful' => true
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:client) do
|
34
|
+
double
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:options) do
|
38
|
+
options = double({prefix: false, file: false})
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:env_vars) do
|
42
|
+
["#comment line", "TEST_ENV_VAR=test", "MYSQL_ADMIN_PASSWORD=abcdef"]
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:dot_env) do
|
46
|
+
["TEST_ENV_VAR=test2","", "TEST_ENV_VAR2=test3"]
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#deploy' do
|
50
|
+
context 'when api_url is nil' do
|
51
|
+
it 'raises error' do
|
52
|
+
allow(subject).to receive(:settings).and_return({'server' => {}})
|
53
|
+
expect{subject.deploy({})}.to raise_error(ArgumentError)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when token is nil' do
|
58
|
+
it 'raises error' do
|
59
|
+
allow(subject).to receive(:settings).and_return({'server' => {'url' => 'http://kontena.test'}})
|
60
|
+
expect{subject.deploy({})}.to raise_error(ArgumentError)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when api url and token are valid' do
|
65
|
+
before(:each) do
|
66
|
+
allow(subject).to receive(:settings).and_return(settings)
|
67
|
+
allow(YAML).to receive(:load).and_return(services)
|
68
|
+
allow(File).to receive(:read)
|
69
|
+
allow(subject).to receive(:get_service).and_raise(Kontena::Errors::StandardError.new(404, 'Not Found'))
|
70
|
+
allow(subject).to receive(:create_service).and_return({'id' => 'kontena-test-mysql'},{'id' => 'kontena-test-wordpress'})
|
71
|
+
allow(subject).to receive(:current_grid).and_return('1')
|
72
|
+
allow(subject).to receive(:deploy_service).and_return(nil)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'reads ./kontena.yml file by default' do
|
76
|
+
allow(subject).to receive(:settings).and_return(settings)
|
77
|
+
|
78
|
+
expect(File).to receive(:read).with('./kontena.yml')
|
79
|
+
expect(options).to receive(:file).once.and_return(false)
|
80
|
+
subject.deploy(options)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'reads given yml file' do
|
84
|
+
expect(options).to receive(:file).once.and_return('custom.yml')
|
85
|
+
expect(File).to receive(:read).with('custom.yml')
|
86
|
+
subject.deploy(options)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'uses current directory as service name prefix by default' do
|
90
|
+
current_dir = '/kontena/tests/stacks'
|
91
|
+
allow(Dir).to receive(:getwd).and_return(current_dir)
|
92
|
+
expect(File).to receive(:basename).with(current_dir)
|
93
|
+
subject.deploy(options)
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when yml file has multiple env files' do
|
97
|
+
it 'merges environment variables correctly' do
|
98
|
+
allow(subject).to receive(:current_dir).and_return("kontena-test")
|
99
|
+
services['wordpress']['environment'] = ['MYSQL_ADMIN_PASSWORD=password']
|
100
|
+
services['wordpress']['env_file'] = %w(/path/to/env_file .env)
|
101
|
+
|
102
|
+
expect(File).to receive(:readlines).with('/path/to/env_file').and_return(env_vars)
|
103
|
+
expect(File).to receive(:readlines).with('.env').and_return(dot_env)
|
104
|
+
|
105
|
+
data = {
|
106
|
+
:name =>"kontena-test-wordpress",
|
107
|
+
:image=>"wordpress:latest",
|
108
|
+
:env=>["MYSQL_ADMIN_PASSWORD=password", "TEST_ENV_VAR=test", "TEST_ENV_VAR2=test3"],
|
109
|
+
:container_count=>2,
|
110
|
+
:stateful=>false,
|
111
|
+
:links=>[{:name=>"kontena-test-mysql", :alias=>"db"}],
|
112
|
+
:ports=>[{:container_port=>"80", :node_port=>"80", :protocol=>"tcp"}]
|
113
|
+
}
|
114
|
+
|
115
|
+
expect(subject).to receive(:create_service).with('1234567', '1', data)
|
116
|
+
subject.deploy(options)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'when yml file has one env file' do
|
121
|
+
it 'merges environment variables correctly' do
|
122
|
+
allow(subject).to receive(:current_dir).and_return("kontena-test")
|
123
|
+
services['wordpress']['environment'] = ['MYSQL_ADMIN_PASSWORD=password']
|
124
|
+
services['wordpress']['env_file'] = '/path/to/env_file'
|
125
|
+
|
126
|
+
expect(File).to receive(:readlines).with('/path/to/env_file').and_return(env_vars)
|
127
|
+
|
128
|
+
data = {
|
129
|
+
:name =>"kontena-test-wordpress",
|
130
|
+
:image=>"wordpress:latest",
|
131
|
+
:env=>["MYSQL_ADMIN_PASSWORD=password", "TEST_ENV_VAR=test"],
|
132
|
+
:container_count=>2,
|
133
|
+
:stateful=>false,
|
134
|
+
:links=>[{:name=>"kontena-test-mysql", :alias=>"db"}],
|
135
|
+
:ports=>[{:container_port=>"80", :node_port=>"80", :protocol=>"tcp"}]
|
136
|
+
}
|
137
|
+
|
138
|
+
expect(subject).to receive(:create_service).with('1234567', '1', data)
|
139
|
+
subject.deploy(options)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'creates mysql service before wordpress' do
|
144
|
+
allow(subject).to receive(:current_dir).and_return("kontena-test")
|
145
|
+
data = {:name =>"kontena-test-mysql", :image=>'mysql:5.6', :env=>nil, :container_count=>nil, :stateful=>true}
|
146
|
+
expect(subject).to receive(:create_service).with('1234567', '1', data)
|
147
|
+
|
148
|
+
subject.deploy(options)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'creates wordpress service' do
|
152
|
+
allow(subject).to receive(:current_dir).and_return("kontena-test")
|
153
|
+
|
154
|
+
data = {
|
155
|
+
:name =>"kontena-test-wordpress",
|
156
|
+
:image=>"wordpress:latest",
|
157
|
+
:env=>nil,
|
158
|
+
:container_count=>2,
|
159
|
+
:stateful=>false,
|
160
|
+
:links=>[{:name=>"kontena-test-mysql", :alias=>"db"}],
|
161
|
+
:ports=>[{:container_port=>"80", :node_port=>"80", :protocol=>"tcp"}]
|
162
|
+
}
|
163
|
+
expect(subject).to receive(:create_service).with('1234567', '1', data)
|
164
|
+
|
165
|
+
subject.deploy(options)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'deploys services' do
|
169
|
+
allow(subject).to receive(:current_dir).and_return("kontena-test")
|
170
|
+
expect(subject).to receive(:deploy_service).with('1234567', 'kontena-test-mysql', {})
|
171
|
+
expect(subject).to receive(:deploy_service).with('1234567', 'kontena-test-wordpress', {:strategy => 'ha'})
|
172
|
+
subject.deploy(options)
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
10
|
+
config.run_all_when_everything_filtered = true
|
11
|
+
config.filter_run :focus
|
12
|
+
|
13
|
+
# Run specs in random order to surface order dependencies. If you find an
|
14
|
+
# order dependency and want to debug it, you can fix the order by providing
|
15
|
+
# the seed, which is printed after each run.
|
16
|
+
# --seed 1234
|
17
|
+
config.order = 'random'
|
18
|
+
|
19
|
+
end
|
data/tasks/rspec.rake
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kontena-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kontena, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -89,12 +89,14 @@ extensions: []
|
|
89
89
|
extra_rdoc_files: []
|
90
90
|
files:
|
91
91
|
- ".gitignore"
|
92
|
+
- Dockerfile
|
92
93
|
- Gemfile
|
93
94
|
- LICENSE.txt
|
94
95
|
- README.md
|
95
96
|
- Rakefile
|
96
97
|
- bin/kontena
|
97
98
|
- kontena-cli.gemspec
|
99
|
+
- kontena-docker.sh
|
98
100
|
- lib/kontena/cli/commands.rb
|
99
101
|
- lib/kontena/cli/common.rb
|
100
102
|
- lib/kontena/cli/containers/commands.rb
|
@@ -103,6 +105,7 @@ files:
|
|
103
105
|
- lib/kontena/cli/grids/commands.rb
|
104
106
|
- lib/kontena/cli/grids/grids.rb
|
105
107
|
- lib/kontena/cli/grids/users.rb
|
108
|
+
- lib/kontena/cli/grids/vpn.rb
|
106
109
|
- lib/kontena/cli/nodes/commands.rb
|
107
110
|
- lib/kontena/cli/nodes/nodes.rb
|
108
111
|
- lib/kontena/cli/server/commands.rb
|
@@ -112,10 +115,17 @@ files:
|
|
112
115
|
- lib/kontena/cli/services/containers.rb
|
113
116
|
- lib/kontena/cli/services/logs.rb
|
114
117
|
- lib/kontena/cli/services/services.rb
|
118
|
+
- lib/kontena/cli/services/services_helper.rb
|
115
119
|
- lib/kontena/cli/services/stats.rb
|
120
|
+
- lib/kontena/cli/stacks/commands.rb
|
121
|
+
- lib/kontena/cli/stacks/stacks.rb
|
116
122
|
- lib/kontena/cli/version.rb
|
117
123
|
- lib/kontena/client.rb
|
118
124
|
- lib/kontena/errors.rb
|
125
|
+
- spec/kontena/cli/services/services_helper_spec.rb
|
126
|
+
- spec/kontena/cli/stacks/stacks_spec.rb
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
- tasks/rspec.rake
|
119
129
|
homepage: http://www.kontena.io
|
120
130
|
licenses:
|
121
131
|
- Apache-2.0
|
@@ -140,4 +150,7 @@ rubygems_version: 2.2.2
|
|
140
150
|
signing_key:
|
141
151
|
specification_version: 4
|
142
152
|
summary: Kontena command line tool
|
143
|
-
test_files:
|
153
|
+
test_files:
|
154
|
+
- spec/kontena/cli/services/services_helper_spec.rb
|
155
|
+
- spec/kontena/cli/stacks/stacks_spec.rb
|
156
|
+
- spec/spec_helper.rb
|