kontena-cli 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|