kontena-plugin-app-command 0.1.0.rc1

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +21 -0
  5. data/Dockerfile +19 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +191 -0
  8. data/README.md +299 -0
  9. data/Rakefile +6 -0
  10. data/kontena-plugin-app-command.gemspec +28 -0
  11. data/lib/kontena/cli/apps/build_command.rb +28 -0
  12. data/lib/kontena/cli/apps/common.rb +172 -0
  13. data/lib/kontena/cli/apps/config_command.rb +25 -0
  14. data/lib/kontena/cli/apps/deploy_command.rb +137 -0
  15. data/lib/kontena/cli/apps/docker_compose_generator.rb +61 -0
  16. data/lib/kontena/cli/apps/docker_helper.rb +80 -0
  17. data/lib/kontena/cli/apps/dockerfile_generator.rb +16 -0
  18. data/lib/kontena/cli/apps/init_command.rb +89 -0
  19. data/lib/kontena/cli/apps/kontena_yml_generator.rb +105 -0
  20. data/lib/kontena/cli/apps/list_command.rb +59 -0
  21. data/lib/kontena/cli/apps/logs_command.rb +37 -0
  22. data/lib/kontena/cli/apps/monitor_command.rb +93 -0
  23. data/lib/kontena/cli/apps/remove_command.rb +74 -0
  24. data/lib/kontena/cli/apps/restart_command.rb +39 -0
  25. data/lib/kontena/cli/apps/scale_command.rb +33 -0
  26. data/lib/kontena/cli/apps/service_generator.rb +114 -0
  27. data/lib/kontena/cli/apps/service_generator_v2.rb +27 -0
  28. data/lib/kontena/cli/apps/show_command.rb +23 -0
  29. data/lib/kontena/cli/apps/start_command.rb +40 -0
  30. data/lib/kontena/cli/apps/stop_command.rb +40 -0
  31. data/lib/kontena/cli/apps/yaml/custom_validators/affinities_validator.rb +19 -0
  32. data/lib/kontena/cli/apps/yaml/custom_validators/build_validator.rb +22 -0
  33. data/lib/kontena/cli/apps/yaml/custom_validators/extends_validator.rb +20 -0
  34. data/lib/kontena/cli/apps/yaml/custom_validators/hooks_validator.rb +54 -0
  35. data/lib/kontena/cli/apps/yaml/custom_validators/secrets_validator.rb +22 -0
  36. data/lib/kontena/cli/apps/yaml/reader.rb +213 -0
  37. data/lib/kontena/cli/apps/yaml/service_extender.rb +77 -0
  38. data/lib/kontena/cli/apps/yaml/validations.rb +71 -0
  39. data/lib/kontena/cli/apps/yaml/validator.rb +38 -0
  40. data/lib/kontena/cli/apps/yaml/validator_v2.rb +53 -0
  41. data/lib/kontena/plugin/app-command/app_command.rb +21 -0
  42. data/lib/kontena/plugin/app-command/version.rb +7 -0
  43. data/lib/kontena_cli_plugin.rb +4 -0
  44. metadata +143 -0
@@ -0,0 +1,20 @@
1
+ module Kontena::Cli::Apps::YAML::Validations::CustomValidators
2
+ class ExtendsValidator < HashValidator::Validator::Base
3
+ def initialize
4
+ super('valid_extends')
5
+ end
6
+
7
+ def validate(key, value, validations, errors)
8
+ unless value.is_a?(String) || value.is_a?(Hash)
9
+ errors[key] = 'extends must be string or hash'
10
+ return
11
+ end
12
+ if value.is_a?(Hash)
13
+ extends_validation = { 'service' => 'string' }
14
+ extends_validation['file'] = HashValidator.optional('string') if value['file']
15
+ extends_validation['stack'] = HashValidator.optional('string') if value['stack']
16
+ HashValidator.validator_for(extends_validation).validate(key, value, extends_validation, errors)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ module Kontena::Cli::Apps::YAML::Validations::CustomValidators
2
+ class HooksValidator < HashValidator::Validator::Base
3
+ def initialize
4
+ super('valid_hooks')
5
+ end
6
+
7
+ def validate(key, value, validations, errors)
8
+ unless value.is_a?(Hash)
9
+ errors[key] = 'hooks must be array'
10
+ return
11
+ end
12
+
13
+ if value['pre_build']
14
+ validate_pre_build_hooks(key, value['pre_build'], errors)
15
+ end
16
+
17
+ if value['post_start']
18
+ validate_post_start_hooks(key, value['post_start'], errors)
19
+ end
20
+ end
21
+
22
+ def validate_pre_build_hooks(key, pre_build_hooks, errors)
23
+ unless pre_build_hooks.is_a?(Array)
24
+ errors[key] = 'pre_build must be array'
25
+ return
26
+ end
27
+ pre_build_validation = {
28
+ 'name' => 'string',
29
+ 'cmd' => 'string'
30
+ }
31
+ validator = HashValidator.validator_for(pre_build_validation)
32
+ pre_build_hooks.each do |pre_build|
33
+ validator.validate('hooks.pre_build', pre_build, pre_build_validation, errors)
34
+ end
35
+ end
36
+
37
+ def validate_post_start_hooks(key, post_start_hooks, errors)
38
+ unless post_start_hooks.is_a?(Array)
39
+ errors[key] = 'post_start must be array'
40
+ return
41
+ end
42
+ post_start_validation = {
43
+ 'name' => 'string',
44
+ 'instances' => (-> (value) { value.is_a?(Integer) || value == '*' }),
45
+ 'cmd' => 'string',
46
+ 'oneshot' => HashValidator.optional('boolean')
47
+ }
48
+ validator = HashValidator.validator_for(post_start_validation)
49
+ post_start_hooks.each do |post_start|
50
+ validator.validate('hooks.post_start', post_start, post_start_validation, errors)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,22 @@
1
+ module Kontena::Cli::Apps::YAML::Validations::CustomValidators
2
+ class SecretsValidator < HashValidator::Validator::Base
3
+ def initialize
4
+ super('valid_secrets')
5
+ end
6
+
7
+ def validate(key, value, validations, errors)
8
+ unless value.is_a?(Array)
9
+ errors[key] = 'secrets must be array'
10
+ return
11
+ end
12
+ secret_item_validation = {
13
+ 'secret' => 'string',
14
+ 'name' => 'string',
15
+ 'type' => 'string'
16
+ }
17
+ value.each do |secret|
18
+ HashValidator.validator_for(secret_item_validation).validate(key, secret, secret_item_validation, errors)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,213 @@
1
+ require_relative 'service_extender'
2
+ require_relative 'validator'
3
+ require_relative 'validator_v2'
4
+ require 'kontena/util'
5
+
6
+ module Kontena::Cli::Apps
7
+ module YAML
8
+ class Reader
9
+ include Kontena::Util
10
+ attr_reader :yaml, :file, :errors, :notifications
11
+
12
+ def initialize(file, skip_validation = false)
13
+ @file = file
14
+ @errors = []
15
+ @notifications = []
16
+ @skip_validation = skip_validation
17
+ load_yaml
18
+ validate unless skip_validation?
19
+ end
20
+
21
+ ##
22
+ # @param [String] service_name
23
+ # @return [Hash]
24
+ def execute(service_name = nil)
25
+ result = {}
26
+ Dir.chdir(File.dirname(File.expand_path(file))) do
27
+ result[:version] = yaml['version'] || '1'
28
+ result[:name] = yaml['name']
29
+ result[:errors] = errors
30
+ result[:notifications] = notifications
31
+ result[:services] = errors.count == 0 ? parse_services(service_name) : {}
32
+ end
33
+ result
34
+ end
35
+
36
+ def stack_name
37
+ yaml['name'] if v2?
38
+ end
39
+
40
+ ##
41
+ # @return [true|false]
42
+ def v2?
43
+ yaml['version'].to_s == '2'
44
+ end
45
+
46
+ private
47
+
48
+ def load_yaml
49
+ content = File.read(File.expand_path(file))
50
+ content = content % { project: ENV['project'], grid: ENV['grid'] }
51
+ interpolate(content)
52
+ replace_dollar_dollars(content)
53
+ begin
54
+ @yaml = ::YAML.safe_load(content)
55
+ rescue Psych::SyntaxError => ex
56
+ raise ex.class, "Error while parsing #{file} : #{ex.message}"
57
+ end
58
+ end
59
+
60
+ # @return [Array] array of validation errors
61
+ def validate
62
+ result = validator.validate(yaml)
63
+ store_failures(result)
64
+ result
65
+ end
66
+
67
+ def skip_validation?
68
+ @skip_validation == true
69
+ end
70
+
71
+ def store_failures(data)
72
+ errors << { file => data[:errors] } unless data[:errors].empty?
73
+ notifications << { file => data[:notifications] } unless data[:notifications].empty?
74
+ end
75
+
76
+ # @return [Kontena::Cli::Apps::YAML::Validator]
77
+ def validator
78
+ if @validator.nil?
79
+ validator_klass = v2? ? YAML::ValidatorV2 : YAML::Validator
80
+ @validator = validator_klass.new
81
+ end
82
+ @validator
83
+ end
84
+
85
+ ##
86
+ # @param [String] service_name - optional service to parse
87
+ # @return [Hash]
88
+ def parse_services(service_name = nil)
89
+ if service_name.nil?
90
+ services.each do |name, config|
91
+ services[name] = process_config(config)
92
+ end
93
+ services
94
+ else
95
+ raise ("Service '#{service_name}' not found in #{file}") unless services.key?(service_name)
96
+ process_config(services[service_name])
97
+ end
98
+ end
99
+
100
+ # @param [Hash] service_config
101
+ def process_config(service_config)
102
+ normalize_env_vars(service_config)
103
+ merge_env_vars(service_config)
104
+ expand_build_context(service_config)
105
+ normalize_build_args(service_config)
106
+ service_config = extend_config(service_config) if service_config.key?('extends')
107
+ service_config
108
+ end
109
+
110
+ # @return [Hash] - services from YAML file
111
+ def services
112
+ if v2?
113
+ yaml['services']
114
+ else
115
+ yaml
116
+ end
117
+ end
118
+
119
+ ##
120
+ # @param [String] text - content of YAML file
121
+ def interpolate(text)
122
+ text.gsub!(/(?<!\$)\$(?!\$)\{?\w+\}?/) do |v| # searches $VAR and ${VAR} and not $$VAR
123
+ var = v.tr('${}', '')
124
+ puts "The #{var} is not set. Substituting an empty string." if !ENV.key?(var) && !skip_validation?
125
+ ENV[var] # replace with equivalent ENV variables
126
+ end
127
+ end
128
+
129
+ ##
130
+ # @param [String] text - content of yaml file
131
+ def replace_dollar_dollars(text)
132
+ text.gsub!('$$', '$')
133
+ end
134
+
135
+ # @param [Hash] service_config
136
+ # @return [Hash] updated service config
137
+ def extend_config(service_config)
138
+ extended_service = extended_service(service_config['extends'])
139
+ return unless extended_service
140
+ filename = service_config['extends']['file']
141
+ if filename
142
+ parent_config = from_external_file(filename, extended_service)
143
+ else
144
+ raise ("Service '#{extended_service}' not found in #{file}") unless services.key?(extended_service)
145
+ parent_config = process_config(services[extended_service])
146
+ end
147
+ ServiceExtender.new(service_config).extend(parent_config)
148
+ end
149
+
150
+ def extended_service(extend_config)
151
+ if extend_config.is_a?(Hash)
152
+ extend_config['service']
153
+ elsif extend_config.is_a?(String)
154
+ extend_config
155
+ else
156
+ nil
157
+ end
158
+ end
159
+
160
+ def from_external_file(filename, service_name)
161
+ outcome = Reader.new(filename, @skip_validation).execute(service_name)
162
+ errors.concat outcome[:errors] unless errors.any? { |item| item.key?(filename) }
163
+ notifications.concat outcome[:notifications] unless notifications.any? { |item| item.key?(filename) }
164
+ outcome[:services]
165
+ end
166
+
167
+ # @param [Hash] options - service config
168
+ def normalize_env_vars(options)
169
+ if options['environment'].is_a?(Hash)
170
+ options['environment'] = options['environment'].map { |k, v| "#{k}=#{v}" }
171
+ end
172
+ end
173
+
174
+ # @param [Hash] options
175
+ def merge_env_vars(options)
176
+ return options['environment'] unless options['env_file']
177
+
178
+ options['env_file'] = [options['env_file']] if options['env_file'].is_a?(String)
179
+ options['environment'] = [] unless options['environment']
180
+ options['env_file'].each do |env_file|
181
+ options['environment'].concat(read_env_file(env_file))
182
+ end
183
+ options.delete('env_file')
184
+ options['environment'].uniq! { |s| s.split('=').first }
185
+ end
186
+
187
+ # @param [String] path
188
+ def read_env_file(path)
189
+ File.readlines(path).map { |line| line.strip }.delete_if { |line| line.start_with?('#') || line.empty? }
190
+ end
191
+
192
+ def expand_build_context(options)
193
+ if options['build'].is_a?(String)
194
+ options['build'] = File.expand_path(options['build'])
195
+ elsif context = options.dig('build', 'context')
196
+ options['build']['context'] = File.expand_path(context)
197
+ end
198
+ end
199
+
200
+ # @param [Hash] options - service config
201
+ def normalize_build_args(options)
202
+ if v2? && safe_dig(options, 'build', 'args').is_a?(Array)
203
+ args = options['build']['args'].dup
204
+ options['build']['args'] = {}
205
+ args.each do |arg|
206
+ k,v = arg.split('=')
207
+ options['build']['args'][k] = v
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,77 @@
1
+ require 'kontena/util'
2
+
3
+ module Kontena::Cli::Apps
4
+ module YAML
5
+ class ServiceExtender
6
+ include Kontena::Util
7
+ attr_reader :service_config
8
+
9
+ # @param [Hash] service_config
10
+ def initialize(service_config)
11
+ @service_config = service_config
12
+ end
13
+
14
+ # @param [Hash] from
15
+ # @return [Hash]
16
+ def extend(from)
17
+ service_config['environment'] = extend_env_vars(
18
+ from['environment'],
19
+ service_config['environment']
20
+ )
21
+ service_config['secrets'] = extend_secrets(
22
+ from['secrets'],
23
+ service_config['secrets']
24
+ )
25
+ build_args = extend_build_args(safe_dig(from, 'build', 'args'), safe_dig(service_config, 'build', 'args'))
26
+ unless build_args.empty?
27
+ service_config['build'] = {} unless service_config['build']
28
+ service_config['build']['args'] = build_args
29
+ end
30
+
31
+ from.merge(service_config)
32
+ end
33
+
34
+ private
35
+
36
+ # @param [Array] from
37
+ # @param [Array] to
38
+ # @return [Array]
39
+ def extend_env_vars(from, to)
40
+ env_vars = to || []
41
+ if from
42
+ from.each do |env|
43
+ env_vars << env unless to && to.find do |key|
44
+ key.split('=').first == env.split('=').first
45
+ end
46
+ end
47
+ end
48
+ env_vars
49
+ end
50
+
51
+ # @param [Array] from
52
+ # @param [Array] to
53
+ # @return [Array]
54
+ def extend_secrets(from, to)
55
+ secrets = to || []
56
+ if from
57
+ from.each do |from_secret|
58
+ secrets << from_secret unless to && to.any? do |to_secret|
59
+ to_secret['secret'] == from_secret['secret']
60
+ end
61
+ end
62
+ end
63
+ secrets
64
+ end
65
+
66
+ def extend_build_args(from, to)
67
+ args = to || {}
68
+ if from
69
+ from.each do |k,v|
70
+ args[k] = v unless args.has_key?(k)
71
+ end
72
+ end
73
+ args
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,71 @@
1
+ module Kontena::Cli::Apps::YAML
2
+ module Validations
3
+ module CustomValidators
4
+ require_relative 'custom_validators/affinities_validator'
5
+ require_relative 'custom_validators/build_validator'
6
+ require_relative 'custom_validators/extends_validator'
7
+ require_relative 'custom_validators/hooks_validator'
8
+ require_relative 'custom_validators/secrets_validator'
9
+
10
+ def self.load
11
+ return if @loaded
12
+ HashValidator.append_validator(AffinitiesValidator.new)
13
+ HashValidator.append_validator(BuildValidator.new)
14
+ HashValidator.append_validator(ExtendsValidator.new)
15
+ HashValidator.append_validator(SecretsValidator.new)
16
+ HashValidator.append_validator(HooksValidator.new)
17
+ @loaded = true
18
+ end
19
+ end
20
+
21
+ def common_validations
22
+ {
23
+ 'image' => optional('string'), # it's optional because some base yml file might contain image option
24
+ 'extends' => optional('valid_extends'),
25
+ 'stateful' => optional('boolean'),
26
+ 'affinity' => optional('valid_affinities'),
27
+ 'cap_add' => optional('array'),
28
+ 'cap_drop' => optional('array'),
29
+ 'command' => optional('string'),
30
+ 'cpu_shares' => optional('integer'),
31
+ 'external_links' => optional('array'),
32
+ 'mem_limit' => optional('string'),
33
+ 'mem_swaplimit' => optional('string'),
34
+ 'environment' => optional(-> (value) { value.is_a?(Array) || value.is_a?(Hash) }),
35
+ 'env_file' => optional(-> (value) { value.is_a?(String) || value.is_a?(Array) }),
36
+ 'instances' => optional('integer'),
37
+ 'links' => optional(-> (value) { value.is_a?(Array) || value.nil? }),
38
+ 'ports' => optional('array'),
39
+ 'pid' => optional('string'),
40
+ 'privileged' => optional('boolean'),
41
+ 'user' => optional('string'),
42
+ 'volumes' => optional('array'),
43
+ 'volumes_from' => optional('array'),
44
+ 'secrets' => optional('valid_secrets'),
45
+ 'hooks' => optional('valid_hooks'),
46
+ 'deploy' => optional({
47
+ 'strategy' => optional(%w(ha daemon random)),
48
+ 'wait_for_port' => optional('integer'),
49
+ 'min_health' => optional('float'),
50
+ 'interval' => optional(/^\d+(min|h|d|)$/)
51
+ }),
52
+ 'health_check' => optional({
53
+ 'protocol' => /^(http|tcp)$/,
54
+ 'port' => 'integer',
55
+ 'uri' => optional(/\/[\S]*/),
56
+ 'timeout' => optional('integer'),
57
+ 'interval' => optional('integer'),
58
+ 'initial_delay' => optional('integer')
59
+ })
60
+ }
61
+ end
62
+
63
+ def optional(type)
64
+ HashValidator.optional(type)
65
+ end
66
+
67
+ def validate_options(service_config)
68
+ HashValidator.validate(service_config, @schema, true)
69
+ end
70
+ end
71
+ end