kontena-plugin-app-command 0.1.0.rc1

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