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.
- 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
|