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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32f7df3c75d9dd5cde332f7cf19267508230be21
4
- data.tar.gz: ed374c9d0f1a69e216ff547d7927b67c9b09ae1b
3
+ metadata.gz: 7da42d25b33946f8887d78d4c1e966ddce0836d4
4
+ data.tar.gz: 9cc5dbbd4eae20285b82d9c06a790560deface6c
5
5
  SHA512:
6
- metadata.gz: 59b51892ecc57e51c305823aa39450a9f07970ac5aa91d23a9a7bec7682f5ec141918f70d87590071c46864e1b46a5b182f055e30bfaebce9e29be88fc548a67
7
- data.tar.gz: 438d344f304461f7d518020125d67882e92067f2dec89a0bd0c4763ad345ff84882abf202a59e5a25a0bd1a2efe61900a8f2968b57894b929230294a612658af
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
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in kontena-cli.gemspec
4
4
  gemspec
5
+ group :development, :test do
6
+ gem "rspec"
7
+ end
data/Rakefile CHANGED
@@ -1,2 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
+ Dir.glob('tasks/*.rake').each { |r| import r }
4
+
5
+ task :default => :spec
6
+
data/kontena-docker.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+
3
+ docker inspect kontena-cli-data > /dev/null 2>&1 ||
4
+ docker create --name kontena-cli-data kontena/cli:latest > /dev/null
5
+
6
+ docker run -it --rm --volumes-from kontena-cli-data kontena/cli:latest $@
@@ -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'
@@ -28,6 +28,10 @@ module Kontena
28
28
  @client
29
29
  end
30
30
 
31
+ def reset_client
32
+ @client = nil
33
+ end
34
+
31
35
  def settings_filename
32
36
  File.join(Dir.home, '/.kontena_client.json')
33
37
  end
@@ -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
- print color('Login Successful', :green)
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
- puts log['data']
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 = client(token).get("services/#{service_id}")
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 |p|
47
- puts " - #{p['alias']}"
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
- result = client(token).post("services/#{service_id}/deploy", data)
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
- if options.link
123
- links = parse_links(options.link)
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
- data[:env] = options.env if options.env
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
- def parse_ports(port_options)
175
- port_options.map{|p|
176
- node_port, container_port, protocol = p.split(':')
177
- if node_port.nil? || container_port.nil?
178
- raise ArgumentError.new("Invalid port value #{p}")
179
- end
180
- {
181
- container_port: container_port,
182
- node_port: node_port,
183
- protocol: protocol || 'tcp'
184
- }
185
- }
186
- end
187
-
188
- def parse_links(link_options)
189
- link_options.map{|l|
190
- service_name, alias_name = l.split(':')
191
- if service_name.nil? || alias_name.nil?
192
- raise ArgumentError.new("Invalid link value #{l}")
193
- end
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
@@ -1,5 +1,5 @@
1
1
  module Kontena
2
2
  module Cli
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -171,7 +171,11 @@ module Kontena
171
171
  end
172
172
 
173
173
  def handle_error_response(response)
174
- raise Kontena::Errors::StandardError.new(response.status, response.body)
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
@@ -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
@@ -0,0 +1,5 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ rescue LoadError
5
+ end
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.5.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-02 00:00:00.000000000 Z
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