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
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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