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,74 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
module Kontena::Cli::Apps
|
4
|
+
class RemoveCommand < Kontena::Command
|
5
|
+
include Kontena::Cli::Common
|
6
|
+
include Kontena::Cli::GridOptions
|
7
|
+
include Common
|
8
|
+
|
9
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
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
|
12
|
+
|
13
|
+
parameter "[SERVICE] ...", "Remove services"
|
14
|
+
|
15
|
+
attr_reader :services
|
16
|
+
|
17
|
+
def execute
|
18
|
+
require_api_url
|
19
|
+
require_token
|
20
|
+
require_config_file(filename)
|
21
|
+
confirm unless forced?
|
22
|
+
|
23
|
+
@services = services_from_yaml(filename, service_list, service_prefix, true)
|
24
|
+
if services.size > 0
|
25
|
+
remove_services(services)
|
26
|
+
elsif !service_list.empty?
|
27
|
+
puts "No such service: #{service_list.join(', ')}".colorize(:red)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def remove_services(services)
|
34
|
+
services.find_all {|service_name, options| options['links'] && options['links'].size > 0 }.each do |service_name, options|
|
35
|
+
delete(service_name, options, false)
|
36
|
+
services.delete(service_name)
|
37
|
+
end
|
38
|
+
services.each do |service_name, options|
|
39
|
+
delete(service_name, options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete(name, options, async = true)
|
44
|
+
unless deleted_services.include?(name)
|
45
|
+
service = get_service(token, prefixed_name(name)) rescue nil
|
46
|
+
if(service)
|
47
|
+
spinner "removing #{pastel.cyan(name)}" do
|
48
|
+
delete_service(token, prefixed_name(name))
|
49
|
+
unless async
|
50
|
+
wait_for_delete_to_finish(service)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
deleted_services << name
|
54
|
+
else
|
55
|
+
warning "No such service #{name}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait_for_delete_to_finish(service)
|
61
|
+
until service.nil?
|
62
|
+
service = get_service(token, service['name']) rescue nil
|
63
|
+
sleep 0.5
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
#
|
69
|
+
# @return [Array]
|
70
|
+
def deleted_services
|
71
|
+
@deleted_services ||= []
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
module Kontena::Cli::Apps
|
4
|
+
class RestartCommand < Kontena::Command
|
5
|
+
include Kontena::Cli::Common
|
6
|
+
include Common
|
7
|
+
|
8
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
9
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
10
|
+
|
11
|
+
parameter "[SERVICE] ...", "Services to start"
|
12
|
+
|
13
|
+
attr_reader :services
|
14
|
+
|
15
|
+
def execute
|
16
|
+
require_config_file(filename)
|
17
|
+
|
18
|
+
@services = services_from_yaml(filename, service_list, service_prefix, true)
|
19
|
+
if services.size > 0
|
20
|
+
restart_services(services)
|
21
|
+
elsif !service_list.empty?
|
22
|
+
puts "No such service: #{service_list.join(', ')}".colorize(:red)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def restart_services(services)
|
28
|
+
services.each do |service_name, opts|
|
29
|
+
if service_exists?(service_name)
|
30
|
+
spinner "Sending restart signal to #{service_name.colorize(:cyan)} " do
|
31
|
+
restart_service(token, prefixed_name(service_name))
|
32
|
+
end
|
33
|
+
else
|
34
|
+
warning "No such service: #{service_name}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
module Kontena::Cli::Apps
|
4
|
+
class ScaleCommand < Kontena::Command
|
5
|
+
include Kontena::Cli::Common
|
6
|
+
include Kontena::Cli::GridOptions
|
7
|
+
include Common
|
8
|
+
|
9
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
10
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
11
|
+
|
12
|
+
parameter "SERVICE", "Service to show"
|
13
|
+
parameter "INSTANCES", "Scales service to given number of instances"
|
14
|
+
|
15
|
+
attr_reader :services
|
16
|
+
|
17
|
+
def execute
|
18
|
+
require_config_file(filename)
|
19
|
+
yml_service = services_from_yaml(filename, [service], service_prefix, true)
|
20
|
+
if yml_service[service]
|
21
|
+
options = yml_service[service]
|
22
|
+
exit_with_error("Service has already instances defined in #{filename}. Please update #{filename} and deploy service instead") if options['instances']
|
23
|
+
spinner "Scaling #{service.colorize(:cyan)} " do
|
24
|
+
deployment = scale_service(require_token, prefixed_name(service), instances)
|
25
|
+
wait_for_deploy_to_finish(token, deployment)
|
26
|
+
end
|
27
|
+
|
28
|
+
else
|
29
|
+
exit_with_error("Service not found")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'kontena/cli/services/services_helper'
|
4
|
+
|
5
|
+
module Kontena::Cli::Apps
|
6
|
+
class ServiceGenerator
|
7
|
+
include Kontena::Cli::Services::ServicesHelper
|
8
|
+
|
9
|
+
attr_reader :service_config
|
10
|
+
|
11
|
+
def initialize(service_config)
|
12
|
+
@service_config = service_config
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# @return [Hash]
|
17
|
+
def generate
|
18
|
+
parse_data(service_config)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
##
|
24
|
+
# @param [Hash] options
|
25
|
+
# @return [Hash]
|
26
|
+
def parse_data(options)
|
27
|
+
data = {}
|
28
|
+
data['instances'] = options['instances']
|
29
|
+
data['image'] = parse_image(options['image'])
|
30
|
+
data['env'] = options['environment'] if options['environment']
|
31
|
+
data['links'] = parse_links(options['links'] || [])
|
32
|
+
data['external_links'] = parse_links(options['external_links'] || [])
|
33
|
+
data['ports'] = parse_stringified_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'] = Shellwords.split(options['command']) 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
|
+
deploy['interval'] = parse_relative_time(deploy_opts['interval']) if deploy_opts.has_key?('interval')
|
56
|
+
unless deploy.empty?
|
57
|
+
data['deploy_opts'] = deploy
|
58
|
+
end
|
59
|
+
data['hooks'] = options['hooks'] || {}
|
60
|
+
data['secrets'] = options['secrets'] if options['secrets']
|
61
|
+
data['build'] = parse_build_options(options) if options['build']
|
62
|
+
health_check = {}
|
63
|
+
health_opts = options['health_check'] || {}
|
64
|
+
health_check['protocol'] = health_opts['protocol'] if health_opts.has_key?('protocol')
|
65
|
+
health_check['uri'] = health_opts['uri'] if health_opts.has_key?('uri')
|
66
|
+
health_check['port'] = health_opts['port'] if health_opts.has_key?('port')
|
67
|
+
health_check['timeout'] = health_opts['timeout'] if health_opts.has_key?('timeout')
|
68
|
+
health_check['interval'] = health_opts['interval'] if health_opts.has_key?('interval')
|
69
|
+
health_check['initial_delay'] = health_opts['initial_delay'] if health_opts.has_key?('initial_delay')
|
70
|
+
unless health_check.empty?
|
71
|
+
data['health_check'] = health_check
|
72
|
+
end
|
73
|
+
data
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [Array<String>] port_options
|
77
|
+
# @return [Array<Hash>]
|
78
|
+
def parse_stringified_ports(port_options)
|
79
|
+
parse_ports(port_options).map {|p|
|
80
|
+
{
|
81
|
+
'ip' => p[:ip],
|
82
|
+
'container_port' => p[:container_port],
|
83
|
+
'node_port' => p[:node_port],
|
84
|
+
'protocol' => p[:protocol]
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [Array<String>] link_options
|
90
|
+
# @return [Array<Hash>]
|
91
|
+
def parse_links(link_options)
|
92
|
+
link_options.map{|l|
|
93
|
+
service_name, alias_name = l.split(':')
|
94
|
+
if service_name.nil?
|
95
|
+
raise ArgumentError.new("Invalid link value #{l}")
|
96
|
+
end
|
97
|
+
alias_name = service_name if alias_name.nil?
|
98
|
+
{
|
99
|
+
'name' => service_name,
|
100
|
+
'alias' => alias_name
|
101
|
+
}
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param [Hash] options
|
106
|
+
# @return [Hash]
|
107
|
+
def parse_build_options(options)
|
108
|
+
build = {}
|
109
|
+
build['context'] = options['build'] if options['build']
|
110
|
+
build['dockerfile'] = options['dockerfile'] if options['dockerfile']
|
111
|
+
build
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,27 @@
|
|
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']['args'] = parse_build_args(options['build']['args']) if options['build']['args']
|
24
|
+
options['build']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
module Kontena::Cli::Apps
|
4
|
+
class ShowCommand < Kontena::Command
|
5
|
+
include Kontena::Cli::Common
|
6
|
+
include Kontena::Cli::GridOptions
|
7
|
+
include Common
|
8
|
+
|
9
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
10
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
11
|
+
|
12
|
+
parameter "SERVICE", "Service to show"
|
13
|
+
|
14
|
+
attr_reader :services
|
15
|
+
|
16
|
+
def execute
|
17
|
+
require_config_file(filename)
|
18
|
+
token = require_token
|
19
|
+
show_service(token, prefixed_name(service))
|
20
|
+
show_service_instances(token, prefixed_name(service))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
module Kontena::Cli::Apps
|
4
|
+
class StartCommand < Kontena::Command
|
5
|
+
include Kontena::Cli::Common
|
6
|
+
include Kontena::Cli::GridOptions
|
7
|
+
include Common
|
8
|
+
|
9
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
10
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
11
|
+
|
12
|
+
parameter "[SERVICE] ...", "Services to start"
|
13
|
+
|
14
|
+
attr_reader :services
|
15
|
+
|
16
|
+
def execute
|
17
|
+
require_config_file(filename)
|
18
|
+
|
19
|
+
@services = services_from_yaml(filename, service_list, service_prefix, true)
|
20
|
+
if services.size > 0
|
21
|
+
start_services(services)
|
22
|
+
elsif !service_list.empty?
|
23
|
+
puts "No such service: #{service_list.join(', ')}".colorize(:red)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_services(services)
|
29
|
+
services.each do |service_name, opts|
|
30
|
+
if service_exists?(service_name)
|
31
|
+
spinner "Starting #{service_name.colorize(:cyan)} " do
|
32
|
+
start_service(token, prefixed_name(service_name))
|
33
|
+
end
|
34
|
+
else
|
35
|
+
warning "No such service: #{service_name}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
module Kontena::Cli::Apps
|
4
|
+
class StopCommand < Kontena::Command
|
5
|
+
include Kontena::Cli::Common
|
6
|
+
include Kontena::Cli::GridOptions
|
7
|
+
include Common
|
8
|
+
|
9
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
10
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
11
|
+
|
12
|
+
parameter "[SERVICE] ...", "Services to stop"
|
13
|
+
|
14
|
+
attr_reader :services
|
15
|
+
|
16
|
+
def execute
|
17
|
+
require_config_file(filename)
|
18
|
+
|
19
|
+
@services = services_from_yaml(filename, service_list, service_prefix, true)
|
20
|
+
if services.size > 0
|
21
|
+
stop_services(services)
|
22
|
+
elsif !service_list.empty?
|
23
|
+
puts "No such service: #{service_list.join(', ')}".colorize(:red)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop_services(services)
|
29
|
+
services.each do |service_name, opts|
|
30
|
+
if service_exists?(service_name)
|
31
|
+
spinner "Sending stop signal to #{service_name.colorize(:cyan)} " do
|
32
|
+
stop_service(token, prefixed_name(service_name))
|
33
|
+
end
|
34
|
+
else
|
35
|
+
warning "No such service: #{service_name}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Kontena::Cli::Apps::YAML::Validations::CustomValidators
|
2
|
+
class AffinitiesValidator < HashValidator::Validator::Base
|
3
|
+
def initialize
|
4
|
+
super('valid_affinities')
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate(key, value, validations, errors)
|
8
|
+
unless value.is_a?(Array)
|
9
|
+
errors[key] = 'affinity must be array'
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
invalid_formats = value.find_all { |a| !a.match(/(?<=\!|\=)=/) }
|
14
|
+
if invalid_formats.count > 0
|
15
|
+
errors[key] = "affinity contains invalid formats: #{invalid_formats.join(', ')}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Kontena::Cli::Apps::YAML::Validations::CustomValidators
|
2
|
+
class BuildValidator < HashValidator::Validator::Base
|
3
|
+
def initialize
|
4
|
+
super('valid_build')
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate(key, value, validations, errors)
|
8
|
+
unless value.is_a?(String) || value.is_a?(Hash)
|
9
|
+
errors[key] = 'build must be string or hash'
|
10
|
+
return
|
11
|
+
end
|
12
|
+
if value.is_a?(Hash)
|
13
|
+
build_validation = {
|
14
|
+
'context' => 'string',
|
15
|
+
'dockerfile' => HashValidator.optional('string'),
|
16
|
+
'args' => HashValidator.optional(-> (value) { value.is_a?(Array) || value.is_a?(Hash) })
|
17
|
+
}
|
18
|
+
HashValidator.validator_for(build_validation).validate(key, value, build_validation, errors)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|