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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.travis.yml +21 -0
- data/Dockerfile +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +191 -0
- data/README.md +299 -0
- data/Rakefile +6 -0
- data/kontena-plugin-app-command.gemspec +28 -0
- data/lib/kontena/cli/apps/build_command.rb +28 -0
- data/lib/kontena/cli/apps/common.rb +172 -0
- data/lib/kontena/cli/apps/config_command.rb +25 -0
- data/lib/kontena/cli/apps/deploy_command.rb +137 -0
- data/lib/kontena/cli/apps/docker_compose_generator.rb +61 -0
- data/lib/kontena/cli/apps/docker_helper.rb +80 -0
- data/lib/kontena/cli/apps/dockerfile_generator.rb +16 -0
- data/lib/kontena/cli/apps/init_command.rb +89 -0
- data/lib/kontena/cli/apps/kontena_yml_generator.rb +105 -0
- data/lib/kontena/cli/apps/list_command.rb +59 -0
- data/lib/kontena/cli/apps/logs_command.rb +37 -0
- data/lib/kontena/cli/apps/monitor_command.rb +93 -0
- data/lib/kontena/cli/apps/remove_command.rb +74 -0
- data/lib/kontena/cli/apps/restart_command.rb +39 -0
- data/lib/kontena/cli/apps/scale_command.rb +33 -0
- data/lib/kontena/cli/apps/service_generator.rb +114 -0
- data/lib/kontena/cli/apps/service_generator_v2.rb +27 -0
- data/lib/kontena/cli/apps/show_command.rb +23 -0
- data/lib/kontena/cli/apps/start_command.rb +40 -0
- data/lib/kontena/cli/apps/stop_command.rb +40 -0
- data/lib/kontena/cli/apps/yaml/custom_validators/affinities_validator.rb +19 -0
- data/lib/kontena/cli/apps/yaml/custom_validators/build_validator.rb +22 -0
- data/lib/kontena/cli/apps/yaml/custom_validators/extends_validator.rb +20 -0
- data/lib/kontena/cli/apps/yaml/custom_validators/hooks_validator.rb +54 -0
- data/lib/kontena/cli/apps/yaml/custom_validators/secrets_validator.rb +22 -0
- data/lib/kontena/cli/apps/yaml/reader.rb +213 -0
- data/lib/kontena/cli/apps/yaml/service_extender.rb +77 -0
- data/lib/kontena/cli/apps/yaml/validations.rb +71 -0
- data/lib/kontena/cli/apps/yaml/validator.rb +38 -0
- data/lib/kontena/cli/apps/yaml/validator_v2.rb +53 -0
- data/lib/kontena/plugin/app-command/app_command.rb +21 -0
- data/lib/kontena/plugin/app-command/version.rb +7 -0
- data/lib/kontena_cli_plugin.rb +4 -0
- 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
|