kontena-cli 0.13.4 → 0.14.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.
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