kontena-cli 0.13.4 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/kontena-cli.gemspec +2 -0
  4. data/lib/kontena/cli/app_command.rb +2 -0
  5. data/lib/kontena/cli/apps/common.rb +80 -74
  6. data/lib/kontena/cli/apps/config_command.rb +29 -0
  7. data/lib/kontena/cli/apps/deploy_command.rb +12 -81
  8. data/lib/kontena/cli/apps/docker_helper.rb +3 -3
  9. data/lib/kontena/cli/apps/init_command.rb +0 -3
  10. data/lib/kontena/cli/apps/list_command.rb +2 -3
  11. data/lib/kontena/cli/apps/logs_command.rb +2 -3
  12. data/lib/kontena/cli/apps/monitor_command.rb +3 -4
  13. data/lib/kontena/cli/apps/remove_command.rb +4 -4
  14. data/lib/kontena/cli/apps/restart_command.rb +2 -3
  15. data/lib/kontena/cli/apps/scale_command.rb +3 -5
  16. data/lib/kontena/cli/apps/service_generator.rb +123 -0
  17. data/lib/kontena/cli/apps/service_generator_v2.rb +26 -0
  18. data/lib/kontena/cli/apps/show_command.rb +1 -2
  19. data/lib/kontena/cli/apps/start_command.rb +2 -3
  20. data/lib/kontena/cli/apps/stop_command.rb +2 -3
  21. data/lib/kontena/cli/apps/yaml/reader.rb +150 -0
  22. data/lib/kontena/cli/apps/yaml/service_extender.rb +60 -0
  23. data/lib/kontena/cli/apps/yaml/validations.rb +79 -0
  24. data/lib/kontena/cli/apps/yaml/validator.rb +55 -0
  25. data/lib/kontena/cli/apps/yaml/validator_v2.rb +74 -0
  26. data/lib/kontena/cli/common.rb +23 -0
  27. data/lib/kontena/cli/etcd/remove_command.rb +2 -0
  28. data/lib/kontena/cli/grids/remove_command.rb +2 -0
  29. data/lib/kontena/cli/grids/users/remove_command.rb +3 -0
  30. data/lib/kontena/cli/master/azure/create_command.rb +0 -2
  31. data/lib/kontena/cli/master/packet/create_command.rb +42 -0
  32. data/lib/kontena/cli/master/packet_command.rb +14 -0
  33. data/lib/kontena/cli/master/upcloud/create_command.rb +39 -0
  34. data/lib/kontena/cli/master/upcloud_command.rb +13 -0
  35. data/lib/kontena/cli/master/users/remove_command.rb +3 -0
  36. data/lib/kontena/cli/master/users/roles/remove_command.rb +2 -0
  37. data/lib/kontena/cli/master_command.rb +4 -0
  38. data/lib/kontena/cli/node_command.rb +4 -0
  39. data/lib/kontena/cli/nodes/azure/create_command.rb +0 -2
  40. data/lib/kontena/cli/nodes/list_command.rb +4 -8
  41. data/lib/kontena/cli/nodes/packet/create_command.rb +35 -0
  42. data/lib/kontena/cli/nodes/packet/restart_command.rb +17 -0
  43. data/lib/kontena/cli/nodes/packet/terminate_command.rb +20 -0
  44. data/lib/kontena/cli/nodes/packet_command.rb +15 -0
  45. data/lib/kontena/cli/nodes/remove_command.rb +2 -0
  46. data/lib/kontena/cli/nodes/show_command.rb +3 -1
  47. data/lib/kontena/cli/nodes/upcloud/create_command.rb +33 -0
  48. data/lib/kontena/cli/nodes/upcloud/restart_command.rb +20 -0
  49. data/lib/kontena/cli/nodes/upcloud/terminate_command.rb +20 -0
  50. data/lib/kontena/cli/nodes/upcloud_command.rb +15 -0
  51. data/lib/kontena/cli/registry/remove_command.rb +3 -0
  52. data/lib/kontena/cli/services/remove_command.rb +2 -0
  53. data/lib/kontena/cli/services/services_helper.rb +1 -0
  54. data/lib/kontena/cli/vault/list_command.rb +2 -0
  55. data/lib/kontena/cli/vault/read_command.rb +2 -0
  56. data/lib/kontena/cli/vault/remove_command.rb +4 -0
  57. data/lib/kontena/cli/vault/update_command.rb +8 -1
  58. data/lib/kontena/cli/vault/write_command.rb +2 -0
  59. data/lib/kontena/cli/vpn/remove_command.rb +3 -0
  60. data/lib/kontena/machine/azure/master_provisioner.rb +2 -2
  61. data/lib/kontena/machine/azure/node_provisioner.rb +7 -4
  62. data/lib/kontena/machine/digital_ocean/node_provisioner.rb +1 -1
  63. data/lib/kontena/machine/packet.rb +17 -0
  64. data/lib/kontena/machine/packet/cloudinit.yml +66 -0
  65. data/lib/kontena/machine/packet/cloudinit_master.yml +118 -0
  66. data/lib/kontena/machine/packet/master_provisioner.rb +93 -0
  67. data/lib/kontena/machine/packet/node_destroyer.rb +42 -0
  68. data/lib/kontena/machine/packet/node_provisioner.rb +77 -0
  69. data/lib/kontena/machine/packet/node_restarter.rb +41 -0
  70. data/lib/kontena/machine/packet/packet_common.rb +89 -0
  71. data/lib/kontena/machine/upcloud.rb +9 -0
  72. data/lib/kontena/machine/upcloud/cloudinit.yml +64 -0
  73. data/lib/kontena/machine/upcloud/cloudinit_master.yml +118 -0
  74. data/lib/kontena/machine/upcloud/master_provisioner.rb +136 -0
  75. data/lib/kontena/machine/upcloud/node_destroyer.rb +82 -0
  76. data/lib/kontena/machine/upcloud/node_provisioner.rb +119 -0
  77. data/lib/kontena/machine/upcloud/node_restarter.rb +47 -0
  78. data/lib/kontena/machine/upcloud/upcloud_common.rb +70 -0
  79. data/lib/kontena/scripts/completer +8 -3
  80. data/spec/fixtures/docker-compose_v2.yml +10 -0
  81. data/spec/fixtures/kontena-invalid.yml +4 -0
  82. data/spec/fixtures/kontena-with-variables.yml +19 -0
  83. data/spec/fixtures/kontena.yml +2 -2
  84. data/spec/fixtures/kontena_v2.yml +35 -0
  85. data/spec/kontena/cli/app/common_spec.rb +39 -101
  86. data/spec/kontena/cli/app/deploy_command_spec.rb +37 -388
  87. data/spec/kontena/cli/app/docker_helper_spec.rb +4 -4
  88. data/spec/kontena/cli/app/service_generator_spec.rb +374 -0
  89. data/spec/kontena/cli/app/service_generator_v2_spec.rb +74 -0
  90. data/spec/kontena/cli/app/yaml/reader_spec.rb +249 -0
  91. data/spec/kontena/cli/app/yaml/service_extender_spec.rb +104 -0
  92. data/spec/kontena/cli/app/yaml/validator_spec.rb +263 -0
  93. data/spec/kontena/cli/app/yaml/validator_v2_spec.rb +309 -0
  94. data/spec/kontena/cli/common_spec.rb +39 -1
  95. data/spec/kontena/cli/master/users/remove_command_spec.rb +9 -0
  96. data/spec/kontena/cli/master/users/roles/remove_command_spec.rb +2 -0
  97. metadata +86 -2
@@ -13,13 +13,12 @@ module Kontena::Cli::Apps
13
13
  option ["-t", "--tail"], :flag, "Tail (follow) logs", default: false
14
14
  parameter "[SERVICE] ...", "Show only specified service logs"
15
15
 
16
- attr_reader :services, :service_prefix
16
+ attr_reader :services
17
17
 
18
18
  def execute
19
19
  require_config_file(filename)
20
20
 
21
- @service_prefix = project_name || current_dir
22
- @services = load_services(filename, service_list, service_prefix)
21
+ @services = services_from_yaml(filename, service_list, service_prefix)
23
22
  if services.size > 0
24
23
  show_logs(services)
25
24
  elsif !service_list.empty?
@@ -11,13 +11,12 @@ module Kontena::Cli::Apps
11
11
 
12
12
  parameter "[SERVICE] ...", "Services to start"
13
13
 
14
- attr_reader :services, :service_prefix
14
+ attr_reader :services
15
15
 
16
16
  def execute
17
17
  require_config_file(filename)
18
-
19
- @service_prefix = project_name || current_dir
20
- @services = load_services(filename, service_list, service_prefix)
18
+
19
+ @services = services_from_yaml(filename, service_list, service_prefix)
21
20
  if services.size > 0
22
21
  show_monitor(services)
23
22
  elsif !service_list.empty?
@@ -8,24 +8,24 @@ module Kontena::Cli::Apps
8
8
 
9
9
  option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
10
10
  option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
11
+ option '--force', :flag, 'Force remove', default: false, attribute_name: :forced
11
12
 
12
13
  parameter "[SERVICE] ...", "Remove services"
13
14
 
14
- attr_reader :services, :service_prefix
15
+ attr_reader :services
15
16
 
16
17
  def execute
17
18
  require_api_url
18
19
  require_token
19
20
  require_config_file(filename)
21
+ confirm unless forced?
20
22
 
21
- @service_prefix = project_name || current_dir
22
- @services = load_services(filename, service_list, service_prefix)
23
+ @services = services_from_yaml(filename, service_list, service_prefix)
23
24
  if services.size > 0
24
25
  remove_services(services)
25
26
  elsif !service_list.empty?
26
27
  puts "No such service: #{service_list.join(', ')}".colorize(:red)
27
28
  end
28
-
29
29
  end
30
30
 
31
31
  private
@@ -10,13 +10,12 @@ module Kontena::Cli::Apps
10
10
 
11
11
  parameter "[SERVICE] ...", "Services to start"
12
12
 
13
- attr_reader :services, :service_prefix
13
+ attr_reader :services
14
14
 
15
15
  def execute
16
16
  require_config_file(filename)
17
17
 
18
- @service_prefix = project_name || current_dir
19
- @services = load_services(filename, service_list, service_prefix)
18
+ @services = services_from_yaml(filename, service_list, service_prefix)
20
19
  if services.size > 0
21
20
  restart_services(services)
22
21
  elsif !service_list.empty?
@@ -12,16 +12,14 @@ module Kontena::Cli::Apps
12
12
  parameter "SERVICE", "Service to show"
13
13
  parameter "INSTANCES", "Scales service to given number of instances"
14
14
 
15
- attr_reader :services, :service_prefix
15
+ attr_reader :services
16
16
 
17
17
  def execute
18
18
  require_config_file(filename)
19
- @service_prefix = project_name || current_dir
20
- yml_service = load_services(filename, [service], service_prefix)
19
+ yml_service = services_from_yaml(filename, [service], service_prefix)
21
20
  if yml_service[service]
22
21
  options = yml_service[service]
23
- abort("Service has already instances defined in #{filename}. Please update #{filename} and deploy service instead") if options['instances']
24
- @service_prefix = project_name || current_dir
22
+ abort("Service has already instances defined in #{filename}. Please update #{filename} and deploy service instead") if options['container_count']
25
23
  scale_service(require_token, prefixed_name(service), instances)
26
24
  else
27
25
  abort("Service not found")
@@ -0,0 +1,123 @@
1
+ require 'yaml'
2
+ require_relative '../services/services_helper'
3
+
4
+ module Kontena::Cli::Apps
5
+ class ServiceGenerator
6
+ include Kontena::Cli::Services::ServicesHelper
7
+
8
+ attr_reader :service_config
9
+
10
+ def initialize(service_config)
11
+ @service_config = service_config
12
+ end
13
+
14
+ ##
15
+ # @return [Hash]
16
+ def generate
17
+ parse_data(service_config)
18
+ end
19
+
20
+ private
21
+
22
+ ##
23
+ # @param [Hash] options
24
+ # @return [Hash]
25
+ def parse_data(options)
26
+ data = {}
27
+ data['container_count'] = options['instances']
28
+ data['image'] = parse_image(options['image'])
29
+ data['env'] = merge_env_vars(options)
30
+ data['container_count'] = options['instances']
31
+ data['links'] = parse_links(options['links'] || [])
32
+ data['external_links'] = parse_links(options['external_links'] || [])
33
+ data['ports'] = parse_ports(options['ports'] || [])
34
+ data['memory'] = parse_memory(options['mem_limit'].to_s) if options['mem_limit']
35
+ data['memory_swap'] = parse_memory(options['memswap_limit'].to_s) if options['memswap_limit']
36
+ data['cpu_shares'] = options['cpu_shares'] if options['cpu_shares']
37
+ data['volumes'] = options['volumes'] || []
38
+ data['volumes_from'] = options['volumes_from'] || []
39
+ data['cmd'] = options['command'].split(" ") if options['command']
40
+ data['affinity'] = options['affinity'] || []
41
+ data['user'] = options['user'] if options['user']
42
+ data['stateful'] = options['stateful'] == true
43
+ data['privileged'] = options['privileged'] unless options['privileged'].nil?
44
+ data['cap_add'] = options['cap_add'] if options['cap_add']
45
+ data['cap_drop'] = options['cap_drop'] if options['cap_drop']
46
+ data['net'] = options['net'] if options['net']
47
+ data['pid'] = options['pid'] if options['pid']
48
+ data['log_driver'] = options['log_driver'] if options['log_driver']
49
+ data['log_opts'] = options['log_opt'] if options['log_opt'] && !options['log_opt'].empty?
50
+ deploy_opts = options['deploy'] || {}
51
+ data['strategy'] = deploy_opts['strategy'] if deploy_opts['strategy']
52
+ deploy = {}
53
+ deploy['wait_for_port'] = deploy_opts['wait_for_port'] if deploy_opts.has_key?('wait_for_port')
54
+ deploy['min_health'] = deploy_opts['min_health'] if deploy_opts.has_key?('min_health')
55
+ unless deploy.empty?
56
+ data['deploy_opts'] = deploy
57
+ end
58
+ data['hooks'] = options['hooks'] || {}
59
+ data['secrets'] = options['secrets'] if options['secrets']
60
+ data['build'] = parse_build_options(options) if options['build']
61
+ data
62
+ end
63
+
64
+ # @param [Hash] options
65
+ def merge_env_vars(options)
66
+ return options['environment'] unless options['env_file']
67
+
68
+ options['env_file'] = [options['env_file']] if options['env_file'].is_a?(String)
69
+ options['environment'] = [] unless options['environment']
70
+ options['env_file'].each do |env_file|
71
+ options['environment'].concat(read_env_file(env_file))
72
+ end
73
+ options['environment'].uniq {|s| s.split('=').first}
74
+ end
75
+
76
+
77
+ # @param [String] path
78
+ def read_env_file(path)
79
+ File.readlines(path).delete_if { |line| line.start_with?('#') || line.empty? }
80
+ end
81
+
82
+ # @param [Array<String>] port_options
83
+ # @return [Array<Hash>]
84
+ def parse_ports(port_options)
85
+ port_options.map{|p|
86
+ node_port, container_port, protocol = p.split(':')
87
+ if node_port.nil? || container_port.nil?
88
+ raise ArgumentError.new("Invalid port value #{p}")
89
+ end
90
+ {
91
+ 'container_port' => container_port,
92
+ 'node_port' => node_port,
93
+ 'protocol' => protocol || 'tcp'
94
+ }
95
+ }
96
+ end
97
+
98
+ # @param [Array<String>] link_options
99
+ # @return [Array<Hash>]
100
+ def parse_links(link_options)
101
+ link_options.map{|l|
102
+ service_name, alias_name = l.split(':')
103
+ if service_name.nil?
104
+ raise ArgumentError.new("Invalid link value #{l}")
105
+ end
106
+ alias_name = service_name if alias_name.nil?
107
+ {
108
+ 'name' => service_name,
109
+ 'alias' => alias_name
110
+ }
111
+ }
112
+ end
113
+
114
+ # @param [Hash] options
115
+ # @return [Hash]
116
+ def parse_build_options(options)
117
+ build = {}
118
+ build['context'] = options['build'] if options['build']
119
+ build['dockerfile'] = options['dockerfile'] if options['dockerfile']
120
+ build
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+ require_relative 'service_generator'
3
+
4
+ module Kontena::Cli::Apps
5
+ class ServiceGeneratorV2 < ServiceGenerator
6
+
7
+ def parse_data(options)
8
+ data = super(options)
9
+ data['net'] = options['network_mode'] if options['network_mode']
10
+ data['log_driver'] = options.dig('logging', 'driver')
11
+ data['log_opts'] = options.dig('logging', 'options')
12
+ if options['depends_on']
13
+ data['links'] ||= []
14
+ data['links'] = (data['links'] + parse_links(options['depends_on'])).uniq
15
+ end
16
+ data
17
+ end
18
+
19
+ def parse_build_options(options)
20
+ unless options['build'].is_a?(Hash)
21
+ options['build'] = { 'context' => options['build']}
22
+ end
23
+ options['build']
24
+ end
25
+ end
26
+ end
@@ -11,12 +11,11 @@ module Kontena::Cli::Apps
11
11
 
12
12
  parameter "SERVICE", "Service to show"
13
13
 
14
- attr_reader :services, :service_prefix
14
+ attr_reader :services
15
15
 
16
16
  def execute
17
17
  require_config_file(filename)
18
18
 
19
- @service_prefix = project_name || current_dir
20
19
  show_service(require_token, prefixed_name(service))
21
20
  end
22
21
  end
@@ -11,13 +11,12 @@ module Kontena::Cli::Apps
11
11
 
12
12
  parameter "[SERVICE] ...", "Services to start"
13
13
 
14
- attr_reader :services, :service_prefix
14
+ attr_reader :services
15
15
 
16
16
  def execute
17
17
  require_config_file(filename)
18
18
 
19
- @service_prefix = project_name || current_dir
20
- @services = load_services(filename, service_list, service_prefix)
19
+ @services = services_from_yaml(filename, service_list, service_prefix)
21
20
  if services.size > 0
22
21
  start_services(services)
23
22
  elsif !service_list.empty?
@@ -11,13 +11,12 @@ module Kontena::Cli::Apps
11
11
 
12
12
  parameter "[SERVICE] ...", "Services to stop"
13
13
 
14
- attr_reader :services, :service_prefix
14
+ attr_reader :services
15
15
 
16
16
  def execute
17
17
  require_config_file(filename)
18
18
 
19
- @service_prefix = project_name || current_dir
20
- @services = load_services(filename, service_list, service_prefix)
19
+ @services = services_from_yaml(filename, service_list, service_prefix)
21
20
  if services.size > 0
22
21
  stop_services(services)
23
22
  elsif !service_list.empty?
@@ -0,0 +1,150 @@
1
+ require 'yaml'
2
+ require_relative 'service_extender'
3
+ require_relative 'validator'
4
+ require_relative 'validator_v2'
5
+
6
+ module Kontena::Cli::Apps
7
+ module YAML
8
+ class Reader
9
+ attr_reader :yaml, :file, :errors, :notifications
10
+
11
+ def initialize(file, skip_validation = false)
12
+ @file = file
13
+ @errors = []
14
+ @notifications = []
15
+ @skip_validation = skip_validation
16
+ load_yaml
17
+ validate unless skip_validation?
18
+ end
19
+
20
+ ##
21
+ # @param [String] service_name
22
+ # @return [Hash]
23
+ def execute(service_name = nil)
24
+ result = {}
25
+ Dir.chdir(File.dirname(File.expand_path(file))) do
26
+ result[:version] = yaml['version'] || '1'
27
+ result[:name] = yaml['name']
28
+ result[:services] = parse_services(service_name)
29
+ result[:errors] = errors
30
+ result[:notifications] = notifications
31
+ end
32
+ result
33
+ end
34
+
35
+ ##
36
+ # @return [true|false]
37
+ def v2?
38
+ yaml['version'].to_s == '2'
39
+ end
40
+
41
+ private
42
+
43
+ def load_yaml
44
+ content = File.read(File.expand_path(file))
45
+ content = content % { project: ENV['project'], grid: ENV['grid'] }
46
+ interpolate(content)
47
+ replace_dollar_dollars(content)
48
+ @yaml = ::YAML.load(content)
49
+ end
50
+
51
+ # @return [Array] array of validation errors
52
+ def validate
53
+ result = validator.validate(yaml)
54
+ store_failures(result)
55
+ result
56
+ end
57
+
58
+ def skip_validation?
59
+ @skip_validation == true
60
+ end
61
+
62
+ def store_failures(data)
63
+ errors << { file => data[:errors] } unless data[:errors].empty?
64
+ notifications << { file => data[:notifications] } unless data[:notifications].empty?
65
+ end
66
+
67
+ # @return [Kontena::Cli::Apps::YAML::Validator]
68
+ def validator
69
+ if @validator.nil?
70
+ validator_klass = v2? ? YAML::ValidatorV2 : YAML::Validator
71
+ @validator = validator_klass.new
72
+ end
73
+ @validator
74
+ end
75
+
76
+ ##
77
+ # @param [String] service_name - optional service to parse
78
+ # @return [Hash]
79
+ def parse_services(service_name = nil)
80
+ if service_name.nil?
81
+ services.each { |name, config| services[name] = process_config(config) }
82
+ services
83
+ else
84
+ abort("Service '#{service_name}' not found in #{file}".colorize(:red)) unless services.key?(service_name)
85
+ process_config(services[service_name])
86
+ end
87
+ end
88
+
89
+ # @param [Hash] service_config
90
+ def process_config(service_config)
91
+ normalize_env_vars(service_config)
92
+ service_config = extend_config(service_config) if service_config.key?('extends')
93
+ service_config
94
+ end
95
+
96
+ # @return [Hash] - services from YAML file
97
+ def services
98
+ if v2?
99
+ yaml['services']
100
+ else
101
+ yaml
102
+ end
103
+ end
104
+
105
+ ##
106
+ # @param [String] text - content of YAML file
107
+ def interpolate(text)
108
+ text.gsub!(/(?<!\$)\$(?!\$)\{?\w+\}?/) do |v| # searches $VAR and ${VAR} and not $$VAR
109
+ var = v.tr('${}', '')
110
+ puts "The #{var} is not set. Substituting an empty string." if !ENV.key?(var) && !skip_validation?
111
+ ENV[var] # replace with equivalent ENV variables
112
+ end
113
+ end
114
+
115
+ ##
116
+ # @param [String] text - content of yaml file
117
+ def replace_dollar_dollars(text)
118
+ text.gsub!('$$', '$')
119
+ end
120
+
121
+ # @param [Hash] service_config
122
+ # @return [Hash] updated service config
123
+ def extend_config(service_config)
124
+ service_name = service_config['extends']['service']
125
+ filename = service_config['extends']['file']
126
+ if filename
127
+ parent_service = from_external_file(filename, service_name)
128
+ else
129
+ abort("Service '#{service_name}' not found in #{file}".colorize(:red)) unless services.key?(service_name)
130
+ parent_service = process_config(services[service_name])
131
+ end
132
+ ServiceExtender.new(service_config).extend(parent_service)
133
+ end
134
+
135
+ def from_external_file(filename, service_name)
136
+ outcome = Reader.new(filename, @skip_validation).execute(service_name)
137
+ errors.concat outcome[:errors] unless errors.any? { |item| item.key?(filename) }
138
+ notifications.concat outcome[:notifications] unless notifications.any? { |item| item.key?(filename) }
139
+ outcome[:services]
140
+ end
141
+
142
+ # @param [Hash] options - service config
143
+ def normalize_env_vars(options)
144
+ if options['environment'].is_a?(Hash)
145
+ options['environment'] = options['environment'].map { |k, v| "#{k}=#{v}" }
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end