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
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'kontena/plugin/app-command/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "kontena-plugin-app-command"
|
8
|
+
spec.version = Kontena::Plugin::AppCommand::VERSION
|
9
|
+
spec.authors = ["Kontena, Inc"]
|
10
|
+
spec.email = ["info@kontena.io"]
|
11
|
+
|
12
|
+
spec.summary = %q{Kontena 'app' subcommand for Kontena CLI 1.4+}
|
13
|
+
spec.description = %q{Restores the "kontena app" subcommand back to Kontena CLI v1.4+}
|
14
|
+
spec.homepage = "https://github.com/kontena/kontena-plugin-app-command"
|
15
|
+
|
16
|
+
spec.license = "Apache-2.0"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'kontena-cli', '>= 1.4.0.pre1'
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
require_relative 'docker_helper'
|
3
|
+
|
4
|
+
module Kontena::Cli::Apps
|
5
|
+
class BuildCommand < Kontena::Command
|
6
|
+
include Kontena::Cli::Common
|
7
|
+
include Common
|
8
|
+
include DockerHelper
|
9
|
+
|
10
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
11
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
12
|
+
option ['--no-cache'], :flag, 'Do not use cache when building the image', default: false
|
13
|
+
option '--skip-validation', :flag, 'Skip YAML file validation', default: false
|
14
|
+
parameter "[SERVICE] ...", "Services to build"
|
15
|
+
|
16
|
+
attr_reader :services
|
17
|
+
|
18
|
+
def execute
|
19
|
+
require_config_file(filename)
|
20
|
+
@services = services_from_yaml(filename, service_list, service_prefix, skip_validation?)
|
21
|
+
if services.none?{ |name, service| service['build'] }
|
22
|
+
error 'Not found any service with build option'
|
23
|
+
abort
|
24
|
+
end
|
25
|
+
process_docker_images(services, true, no_cache?)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'kontena/cli/services/services_helper'
|
2
|
+
require_relative './service_generator'
|
3
|
+
require_relative './service_generator_v2'
|
4
|
+
require_relative './yaml/reader'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module Kontena::Cli::Apps
|
8
|
+
module Common
|
9
|
+
include Kontena::Cli::Services::ServicesHelper
|
10
|
+
|
11
|
+
def require_config_file(filename)
|
12
|
+
exit_with_error("File #{filename} does not exist") unless File.exists?(filename)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [String] filename
|
16
|
+
# @param [Array<String>] service_list
|
17
|
+
# @param [String] prefix
|
18
|
+
# @param [TrueClass|FalseClass] skip_validation
|
19
|
+
# @return [Hash]
|
20
|
+
def services_from_yaml(filename, service_list, prefix, skip_validation = false)
|
21
|
+
set_env_variables(prefix, current_grid)
|
22
|
+
reader = YAML::Reader.new(filename, skip_validation)
|
23
|
+
outcome = reader.execute
|
24
|
+
hint_on_validation_notifications(outcome[:notifications]) if outcome[:notifications].size > 0
|
25
|
+
abort_on_validation_errors(outcome[:errors]) if outcome[:errors].size > 0
|
26
|
+
kontena_services = generate_services(outcome[:services], outcome[:version])
|
27
|
+
kontena_services.delete_if { |name, service| !service_list.include?(name)} unless service_list.empty?
|
28
|
+
kontena_services
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# @param [Hash] yaml
|
33
|
+
# @param [String] version
|
34
|
+
# @return [Hash]
|
35
|
+
def generate_services(yaml_services, version)
|
36
|
+
services = {}
|
37
|
+
if version.to_i == 2
|
38
|
+
generator_klass = ServiceGeneratorV2
|
39
|
+
else
|
40
|
+
generator_klass = ServiceGenerator
|
41
|
+
end
|
42
|
+
yaml_services.each do |service_name, config|
|
43
|
+
exit_with_error("Image is missing for #{service_name}. Aborting.") unless config['image']
|
44
|
+
services[service_name] = generator_klass.new(config).generate
|
45
|
+
end
|
46
|
+
services
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_yaml(filename)
|
50
|
+
reader = YAML::Reader.new(filename)
|
51
|
+
outcome = reader.execute
|
52
|
+
outcome
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_env_variables(project, grid)
|
56
|
+
ENV['project'] = project
|
57
|
+
ENV['grid'] = grid
|
58
|
+
end
|
59
|
+
|
60
|
+
def service_prefix
|
61
|
+
@service_prefix ||= project_name || project_name_from_yaml(filename) || current_dir
|
62
|
+
end
|
63
|
+
|
64
|
+
def project_name_from_yaml(file)
|
65
|
+
reader = YAML::Reader.new(file, true)
|
66
|
+
reader.stack_name
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [String]
|
70
|
+
def token
|
71
|
+
@token ||= require_token
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param [String] name
|
75
|
+
# @return [String]
|
76
|
+
def prefixed_name(name)
|
77
|
+
return name if service_prefix.strip == ""
|
78
|
+
"#{service_prefix}-#{name}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def current_dir
|
83
|
+
File.basename(Dir.getwd)
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param [String] name
|
87
|
+
# @return [Boolean]
|
88
|
+
def service_exists?(name)
|
89
|
+
get_service(token, prefixed_name(name)) rescue false
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param [Hash] services
|
93
|
+
# @param [String] file
|
94
|
+
def create_yml(services, file = 'kontena.yml')
|
95
|
+
yml = File.new(file, 'w')
|
96
|
+
yml.puts services.to_yaml
|
97
|
+
yml.close
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Hash]
|
101
|
+
def app_json
|
102
|
+
if !@app_json && File.exist?('app.json')
|
103
|
+
@app_json = JSON.parse(File.read('app.json'))
|
104
|
+
end
|
105
|
+
@app_json ||= {}
|
106
|
+
end
|
107
|
+
|
108
|
+
def display_notifications(messages, color = :yellow)
|
109
|
+
messages.each do |files|
|
110
|
+
files.each do |file, services|
|
111
|
+
$stderr.puts "#{file}:".colorize(color)
|
112
|
+
services.each do |service|
|
113
|
+
service.each do |name, errors|
|
114
|
+
$stderr.puts " #{name}:".colorize(color)
|
115
|
+
if errors.is_a?(String)
|
116
|
+
$stderr.puts " - #{errors}".colorize(color)
|
117
|
+
else
|
118
|
+
errors.each do |key, error|
|
119
|
+
$stderr.puts " - #{key}: #{error.to_json}".colorize(color)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def hint_on_validation_notifications(errors)
|
129
|
+
$stderr.puts "YAML contains the following unsupported options and they were rejected:".colorize(:yellow)
|
130
|
+
display_notifications(errors)
|
131
|
+
end
|
132
|
+
|
133
|
+
def abort_on_validation_errors(errors)
|
134
|
+
$stderr.puts "YAML validation failed! Aborting.".colorize(:red)
|
135
|
+
display_notifications(errors, :red)
|
136
|
+
abort
|
137
|
+
end
|
138
|
+
|
139
|
+
def valid_addons(prefix=nil)
|
140
|
+
if prefix
|
141
|
+
prefix = "#{prefix}-"
|
142
|
+
end
|
143
|
+
|
144
|
+
{
|
145
|
+
'openredis' => {
|
146
|
+
'image' => 'redis:latest',
|
147
|
+
'environment' => ["REDIS_URL=redis://#{prefix}openredis:6379"]
|
148
|
+
},
|
149
|
+
'redis' => {
|
150
|
+
'image' => 'redis:latest',
|
151
|
+
'environment' => ["REDIS_URL=redis://#{prefix}redis:6379"]
|
152
|
+
},
|
153
|
+
'rediscloud' => {
|
154
|
+
'image' => 'redis:latest',
|
155
|
+
'environment' => ["REDISCLOUD_URL=redis://#{prefix}rediscloud:6379"]
|
156
|
+
},
|
157
|
+
'postgresql' => {
|
158
|
+
'image' => 'postgres:latest',
|
159
|
+
'environment' => ["DATABASE_URL=postgres://#{prefix}postgres:@postgresql:5432/postgres"]
|
160
|
+
},
|
161
|
+
'mongolab' => {
|
162
|
+
'image' => 'mongo:latest',
|
163
|
+
'environment' => ["MONGOLAB_URI=#{prefix}mongolab:27017"]
|
164
|
+
},
|
165
|
+
'memcachedcloud' => {
|
166
|
+
'image' => 'memcached:latest',
|
167
|
+
'environment' => ["MEMCACHEDCLOUD_SERVERS=#{prefix}memcachedcloud:11211"]
|
168
|
+
}
|
169
|
+
}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Kontena::Cli::Apps
|
5
|
+
class ConfigCommand < Kontena::Command
|
6
|
+
include Kontena::Cli::Common
|
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 '--skip-validation', :flag, 'Skip YAML file validation', default: false
|
12
|
+
parameter "[SERVICE] ...", "Services to view"
|
13
|
+
|
14
|
+
def execute
|
15
|
+
require_config_file(filename)
|
16
|
+
services = services_from_yaml(filename, service_list, service_prefix, skip_validation?)
|
17
|
+
services.each do |name, config|
|
18
|
+
config['cmd'] = config['cmd'].join(" ") if config['cmd']
|
19
|
+
config.delete_if {|key, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
20
|
+
end
|
21
|
+
services = { 'services' => services }
|
22
|
+
puts services.to_yaml
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'common'
|
3
|
+
require_relative 'docker_helper'
|
4
|
+
|
5
|
+
module Kontena::Cli::Apps
|
6
|
+
class DeployCommand < Kontena::Command
|
7
|
+
include Kontena::Cli::Common
|
8
|
+
include Kontena::Cli::GridOptions
|
9
|
+
include Common
|
10
|
+
include DockerHelper
|
11
|
+
|
12
|
+
option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
|
13
|
+
option ['--no-build'], :flag, 'Don\'t build an image, even if it\'s missing', default: false
|
14
|
+
option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
|
15
|
+
option '--async', :flag, 'Run deploys async/parallel'
|
16
|
+
option '--force', :flag, 'Force deploy even if service does not have any changes'
|
17
|
+
|
18
|
+
option '--skip-validation', :flag, 'Skip YAML file validation', default: false
|
19
|
+
parameter "[SERVICE] ...", "Services to start"
|
20
|
+
|
21
|
+
attr_reader :services, :deploy_queue
|
22
|
+
|
23
|
+
def execute
|
24
|
+
require_api_url
|
25
|
+
require_token
|
26
|
+
require_config_file(filename)
|
27
|
+
@deploy_queue = []
|
28
|
+
@services = services_from_yaml(filename, service_list, service_prefix)
|
29
|
+
process_docker_images(services) if !no_build?
|
30
|
+
create_or_update_services(services)
|
31
|
+
deploy_services(deploy_queue)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @param [Hash] services
|
37
|
+
def create_or_update_services(services)
|
38
|
+
services.each do |name, config|
|
39
|
+
create_or_update_service(name, config)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Array] queue
|
44
|
+
def deploy_services(queue)
|
45
|
+
queue.each do |service|
|
46
|
+
name = service['id'].split('/').last
|
47
|
+
options = {}
|
48
|
+
options[:force] = true if force?
|
49
|
+
spinner "Deploying #{unprefixed_name(name).colorize(:cyan)} " do
|
50
|
+
deployment = deploy_service(token, name, options)
|
51
|
+
unless async?
|
52
|
+
wait_for_deploy_to_finish(token, deployment)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [String] name
|
59
|
+
# @param [Hash] options
|
60
|
+
def create_or_update_service(name, options)
|
61
|
+
# skip if service is already processed or it's not present
|
62
|
+
return nil if in_deploy_queue?(name) || !services.key?(name)
|
63
|
+
|
64
|
+
# create/update linked services recursively before continuing
|
65
|
+
unless options['links'].empty?
|
66
|
+
options['links'].each_with_index do |linked_service, index|
|
67
|
+
# change prefixed service name also to links options
|
68
|
+
linked_service_name = linked_service['name']
|
69
|
+
options['links'][index]['name'] = "#{prefixed_name(linked_service['name'])}"
|
70
|
+
create_or_update_service(linked_service_name, services[linked_service_name]) unless in_deploy_queue?(linked_service_name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
merge_external_links(options)
|
75
|
+
|
76
|
+
if service_exists?(name)
|
77
|
+
service = update(name, options)
|
78
|
+
else
|
79
|
+
service = create(name, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
deploy_queue.push service
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [String] name
|
86
|
+
def find_service_by_name(name)
|
87
|
+
get_service(token, prefixed_name(name)) rescue nil
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param [String] name
|
91
|
+
# @param [Hash] options
|
92
|
+
def create(name, options)
|
93
|
+
data = { 'name' => prefixed_name(name) }
|
94
|
+
data.merge!(options)
|
95
|
+
result = nil
|
96
|
+
spinner "Creating #{name.colorize(:cyan)} " do
|
97
|
+
result = create_service(token, current_grid, data)
|
98
|
+
end
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param [String] name
|
103
|
+
# @param [Hash] options
|
104
|
+
def update(name, options)
|
105
|
+
prefixed_name = prefixed_name(name)
|
106
|
+
result = nil
|
107
|
+
spinner "Updating #{name.colorize(:cyan)} " do
|
108
|
+
result = update_service(token, prefixed_name, options)
|
109
|
+
end
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [String] name
|
114
|
+
def in_deploy_queue?(name)
|
115
|
+
deploy_queue.find {|service| service['name'] == prefixed_name(name)} != nil
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# @param [String] name
|
120
|
+
def unprefixed_name(name)
|
121
|
+
if service_prefix.empty?
|
122
|
+
name
|
123
|
+
else
|
124
|
+
name.sub("#{service_prefix}-", '')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @param [Hash] options
|
129
|
+
def merge_external_links(options)
|
130
|
+
if options['external_links']
|
131
|
+
options['links'] ||= []
|
132
|
+
options['links'] = options['links'] + options['external_links']
|
133
|
+
options.delete('external_links')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'common'
|
3
|
+
|
4
|
+
module Kontena::Cli::Apps
|
5
|
+
class DockerComposeGenerator
|
6
|
+
include Common
|
7
|
+
|
8
|
+
attr_reader :docker_compose_file
|
9
|
+
|
10
|
+
def initialize(filename)
|
11
|
+
@docker_compose_file = filename
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate(procfile, addons, env_file)
|
15
|
+
if procfile.keys.size > 0
|
16
|
+
# generate services found in Procfile
|
17
|
+
docker_compose = {
|
18
|
+
'version' => '2'
|
19
|
+
}
|
20
|
+
services = {}
|
21
|
+
procfile.each do |service, command|
|
22
|
+
services[service] = {'build' => '.' }
|
23
|
+
if app_json && service == 'web' # Heroku generates PORT env variable so should we do too
|
24
|
+
services[service]['environment'] = ['PORT=5000']
|
25
|
+
services[service]['ports'] = ['5000:5000']
|
26
|
+
end
|
27
|
+
services[service]['command'] = "/start #{service}" if service != 'web'
|
28
|
+
services[service]['env_file'] = env_file if env_file
|
29
|
+
|
30
|
+
# generate addon services
|
31
|
+
addons.each do |addon|
|
32
|
+
addon_service = addon.split(":")[0]
|
33
|
+
addon_service.slice!('heroku-')
|
34
|
+
if valid_addons.has_key?(addon_service)
|
35
|
+
services[service]['links'] = [] unless services[service]['links']
|
36
|
+
services[service]['links'] << "#{addon_service}:#{addon_service}"
|
37
|
+
services[service]['environment'] = [] unless services[service]['environment']
|
38
|
+
services[service]['environment'] += valid_addons[addon_service]['environment']
|
39
|
+
services[addon_service] = {'image' => valid_addons[addon_service]['image']}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
docker_compose['services'] = services
|
44
|
+
else
|
45
|
+
# no Procfile found, create dummy web service
|
46
|
+
docker_compose = {
|
47
|
+
'version' => '2',
|
48
|
+
'services' => {
|
49
|
+
'web' => {
|
50
|
+
'build' => '.'
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
docker_compose['services']['web']['env_file'] = env_file if env_file
|
56
|
+
end
|
57
|
+
# create docker-compose.yml file
|
58
|
+
create_yml(docker_compose, docker_compose_file)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|