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