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
@@ -0,0 +1,80 @@
1
+ module Kontena::Cli::Apps
2
+ module DockerHelper
3
+
4
+ # @param [Hash] services
5
+ # @param [Boolean] force_build
6
+ # @param [Boolean] no_cache
7
+ def process_docker_images(services, force_build = false, no_cache = false)
8
+ services.each do |name, service|
9
+ if service['build'] && (!image_exist?(service['image']) || force_build)
10
+ dockerfile = service['build']['dockerfile'] || 'Dockerfile'
11
+ raise ("'#{service['image']}' is not valid Docker image name") unless validate_image_name(service['image'])
12
+ raise ("'#{service['build']['context']}' does not have #{dockerfile}") unless dockerfile_exist?(service['build']['context'], dockerfile)
13
+ if service['hooks'] && service['hooks']['pre_build']
14
+ puts "Running pre_build hook".colorize(:cyan)
15
+ run_pre_build_hook(service['hooks']['pre_build'])
16
+ end
17
+ puts "Building image #{service['image'].colorize(:cyan)}"
18
+ build_docker_image(service, no_cache)
19
+ puts "Pushing image #{service['image'].colorize(:cyan)} to registry"
20
+ push_docker_image(service['image'])
21
+ end
22
+ end
23
+ end
24
+
25
+ # @param [String] name
26
+ # @return [Boolean]
27
+ def validate_image_name(name)
28
+ !(/^[\w.\/\-:]+:?+[\w+.]+$/ =~ name).nil?
29
+ end
30
+
31
+ # @param [Hash] service
32
+ # @param [Boolean] no_cache
33
+ # @return [Integer]
34
+ def build_docker_image(service, no_cache = false)
35
+ dockerfile = dockerfile = service['build']['dockerfile'] || 'Dockerfile'
36
+ build_context = service['build']['context']
37
+ cmd = ['docker', 'build', '-t', service['image']]
38
+ cmd << ['-f', File.join(File.expand_path(build_context), dockerfile)] if dockerfile != "Dockerfile"
39
+ cmd << '--no-cache' if no_cache
40
+ args = service['build']['args'] || {}
41
+ args.each do |k, v|
42
+ cmd << "--build-arg=#{k}=#{v}"
43
+ end
44
+ cmd << build_context
45
+ ret = system(*cmd.flatten)
46
+ raise ("Failed to build image #{service['image'].colorize(:cyan)}") unless ret
47
+ ret
48
+ end
49
+
50
+ # @param [String] image
51
+ # @return [Integer]
52
+ def push_docker_image(image)
53
+ ret = system('docker', 'push', image)
54
+ raise ("Failed to push image #{image.colorize(:cyan)}") unless ret
55
+ ret
56
+ end
57
+
58
+ # @param [String] image
59
+ # @return [Boolean]
60
+ def image_exist?(image)
61
+ system("docker history '#{image}' >/dev/null 2>/dev/null")
62
+ end
63
+
64
+ # @param [String] path
65
+ # @param [String] dockerfile
66
+ # @return [Boolean]
67
+ def dockerfile_exist?(path, dockerfile)
68
+ file = File.join(File.expand_path(path), dockerfile)
69
+ File.exist?(file)
70
+ end
71
+
72
+ # @param [Hash] hook
73
+ def run_pre_build_hook(hook)
74
+ hook.each do |h|
75
+ ret = system(h['cmd'])
76
+ raise ("Failed to run pre_build hook: #{h['name']}!") unless ret
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,16 @@
1
+ require 'yaml'
2
+ require_relative 'common'
3
+
4
+ module Kontena::Cli::Apps
5
+ class DockerfileGenerator
6
+ include Common
7
+
8
+ def generate(base_image)
9
+
10
+ dockerfile = File.new('Dockerfile', 'w')
11
+ dockerfile.puts "FROM #{base_image}"
12
+ dockerfile.puts 'CMD ["/start", "web"]'
13
+ dockerfile.close
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,89 @@
1
+ require 'securerandom'
2
+ require_relative 'common'
3
+ require_relative 'dockerfile_generator'
4
+ require_relative 'docker_compose_generator'
5
+ require_relative 'kontena_yml_generator'
6
+
7
+ module Kontena::Cli::Apps
8
+ class InitCommand < Kontena::Command
9
+ include Kontena::Cli::Common
10
+ include Common
11
+
12
+ option ["-f", "--file"], "FILE", "Specify a docker-compose file", attribute_name: :docker_compose_file, default: 'docker-compose.yml'
13
+ option ["-i", "--image-name"], "IMAGE_NAME", "Specify a docker image name"
14
+ option ["-b", "--base-image"], "BASE_IMAGE_NAME", "Specify a docker base image name", default: "kontena/buildstep"
15
+ option ["-p", "--project-name"], "NAME", "Specify an alternate project name (default: directory name)"
16
+
17
+
18
+ def execute
19
+ if File.exist?('Dockerfile')
20
+ puts 'Found Dockerfile'
21
+ elsif create_dockerfile?
22
+ puts "Creating #{'Dockerfile'.colorize(:cyan)}"
23
+ DockerfileGenerator.new.generate(base_image)
24
+ end
25
+
26
+ if File.exist?('Procfile')
27
+ procfile = ::YAML.safe_load(File.read('Procfile'))
28
+ else
29
+ procfile = {}
30
+ end
31
+
32
+ app_env = create_env_file(app_json['env']) if app_json['env']
33
+ addons = app_json['addons'] || []
34
+
35
+ if File.exist?(docker_compose_file)
36
+ puts "Found #{docker_compose_file}."
37
+ elsif create_docker_compose_yml?
38
+ puts "Creating #{docker_compose_file.colorize(:cyan)}"
39
+ docker_compose_generator = DockerComposeGenerator.new(docker_compose_file)
40
+ docker_compose_generator.generate(procfile, addons, app_env)
41
+ end
42
+
43
+ if File.exist?('kontena.yml')
44
+ puts "Updating #{'kontena.yml'.colorize(:cyan)}"
45
+ else
46
+ puts "Creating #{'kontena.yml'.colorize(:cyan)}"
47
+ end
48
+
49
+ kontena_yml_generator = KontenaYmlGenerator.new(image_name, service_prefix)
50
+ if File.exist?(docker_compose_file)
51
+ kontena_yml_generator.generate_from_compose_file(docker_compose_file)
52
+ else
53
+ kontena_yml_generator.generate(procfile, addons, app_env)
54
+ end
55
+
56
+ puts "Your app is ready! Deploy with 'kontena app deploy'.".colorize(:green)
57
+ end
58
+
59
+
60
+ protected
61
+
62
+ def service_prefix
63
+ @service_prefix ||= project_name || current_dir
64
+ end
65
+
66
+ def create_dockerfile?
67
+ prompt.yes?('Dockerfile not found. Do you want to create it?')
68
+ end
69
+
70
+ def create_env_file(env)
71
+ app_env = File.new('.env', 'w')
72
+ app_json['env'].each do |key, env|
73
+ if env['generator'] == 'secret'
74
+ value = SecureRandom.hex(64)
75
+ else
76
+ value = env['value']
77
+ end
78
+ app_env.puts "#{key}=#{value}"
79
+ end
80
+ app_env.close
81
+ '.env'
82
+ end
83
+
84
+ def create_docker_compose_yml?
85
+ prompt.yes?("#{docker_compose_file} not found. Do you want to create it?")
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,105 @@
1
+ require_relative 'common'
2
+
3
+ module Kontena::Cli::Apps
4
+ class KontenaYmlGenerator
5
+ include Common
6
+
7
+ attr_reader :image_name, :service_prefix
8
+
9
+ def initialize(image_name, service_prefix)
10
+ @image_name = image_name
11
+ @service_prefix = service_prefix
12
+ end
13
+
14
+ def generate_from_compose_file(docker_compose_file)
15
+ services = {}
16
+ # extend services from docker-compose.yml
17
+ file = File.read(docker_compose_file)
18
+
19
+ yml_services(file).each do |name, options|
20
+ services[name] = {'extends' => { 'file' => 'docker-compose.yml', 'service' => name }}
21
+ if options.has_key?('build')
22
+ image = image_name || "registry.kontena.local/#{File.basename(Dir.getwd)}:latest"
23
+ services[name]['image'] = image
24
+ end
25
+
26
+ # set Heroku addon service as stateful by default
27
+ if valid_addons.has_key?(name)
28
+ services[name]['stateful'] = true
29
+ end
30
+
31
+ # we have to generate Kontena urls to env vars for Heroku addons
32
+ # redis://openredis:6379 -> redis://project-name-openredis:6379
33
+ if options['links']
34
+ options['links'].each do |link|
35
+ service_link = link.split(':').first
36
+ if valid_addons.has_key?(service_link)
37
+ services[name]['environment'] ||= []
38
+ services[name]['environment'] += valid_addons(service_prefix)[service_link]['environment']
39
+ end
40
+ end
41
+ end
42
+ end
43
+ create_yml(services, 'kontena.yml')
44
+ end
45
+
46
+ def yml_services(file)
47
+ yml = ::YAML.safe_load(file)
48
+ if yml['version'] == '2'
49
+ yml['services']
50
+ else
51
+ yml
52
+ end
53
+ end
54
+
55
+ def generate(procfile, addons, env_file)
56
+ image = image_name || "registry.kontena.local/#{File.basename(Dir.getwd)}:latest"
57
+ if procfile.keys.size > 0
58
+ # generate services found in Procfile
59
+ services = {}
60
+ procfile.keys.each do |name|
61
+ services[name] = {'image' => image}
62
+ services[name]['environment'] = ['PORT=5000'] if app_json && name == 'web' # Heroku generates PORT env variable so should we do too
63
+ services[name]['command'] = "/start #{name}" if name != 'web'
64
+ services[name]['env_file'] = env_file if env_file
65
+
66
+ # generate addon services
67
+ addons.each do |addon|
68
+ addon_service = addon.split(":")[0]
69
+ addon_service.slice!('heroku-')
70
+ if valid_addons.has_key?(addon_service)
71
+ services[name]['links'] ||= []
72
+ services[name]['links'] << "#{addon_service}:#{addon_service}"
73
+ services[name]['environment'] ||= []
74
+ services[name]['environment'] += valid_addons(service_prefix)[addon_service]['environment']
75
+ services[addon_service] = {'image' => valid_addons[addon_service]['image'], 'stateful' => true}
76
+ end
77
+ end
78
+ end
79
+ else
80
+ # no Procfile found, create dummy web service
81
+ services = {'web' => { 'image' => image}}
82
+ services['web']['env_file'] = env_file if env_file
83
+ end
84
+ # create kontena.yml file
85
+ create_yml(services, 'kontena.yml')
86
+ end
87
+
88
+ def create_yml(services, filename)
89
+ if File.exist?(filename) && !File.zero?(filename)
90
+ kontena_services = yml_services(File.read(filename))
91
+ services.each do |name, options|
92
+ if kontena_services[name]
93
+ services[name].merge!(kontena_services[name])
94
+ end
95
+ end
96
+ end
97
+ kontena_services = {
98
+ 'version' => '2',
99
+ 'name' => service_prefix,
100
+ 'services' => services
101
+ }
102
+ super(kontena_services, filename)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'common'
2
+
3
+ module Kontena::Cli::Apps
4
+ class ListCommand < 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
+ option ['-q', '--quiet'], :flag, "Output the identifying column only"
13
+
14
+ parameter "[SERVICE] ...", "Services to list"
15
+
16
+ attr_reader :services
17
+
18
+ def execute
19
+ require_config_file(filename)
20
+
21
+ @services = services_from_yaml(filename, service_list, service_prefix, true)
22
+
23
+ if quiet?
24
+ puts services.map(&:first).join("\n")
25
+ exit 0
26
+ end
27
+
28
+ if services.size > 0
29
+ show_services(services)
30
+ elsif !service_list.empty?
31
+ puts "No such service: #{service_list.join(', ')}".colorize(:red)
32
+ end
33
+
34
+ end
35
+
36
+ def show_services(services)
37
+ titles = ['NAME', 'IMAGE', 'INSTANCES', 'STATEFUL', 'STATE', 'PORTS']
38
+ puts "%-30.30s %-50.50s %-15s %-10.10s %-15.20s %-50s" % titles
39
+
40
+ services.each do |service_name, opts|
41
+ service = get_service(token, prefixed_name(service_name)) rescue false
42
+ if service
43
+ name = service['name'].sub("#{service_prefix}-", '')
44
+ state = service['stateful'] ? 'yes' : 'no'
45
+ ports = service['ports'].map{|p|
46
+ "#{p['ip']}:#{p['node_port']}->#{p['container_port']}/#{p['protocol']}"
47
+ }.join(", ")
48
+ running = service['instance_counts']['running']
49
+ desired = service['instances']
50
+ instances = "#{running} / #{desired}"
51
+ vars = [name, service['image'], instances, state, service['state'], ports]
52
+ else
53
+ vars = [service_name, '-', '-', '-', '-', '-']
54
+ end
55
+ puts "%-30.30s %-50.50s %-15.10s %-10.10s %-15.20s %-50s" % vars
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'common'
2
+ require 'kontena/cli/helpers/log_helper'
3
+
4
+ module Kontena::Cli::Apps
5
+ class LogsCommand < Kontena::Command
6
+ include Kontena::Cli::Common
7
+ include Kontena::Cli::GridOptions
8
+ include Kontena::Cli::Helpers::LogHelper
9
+
10
+ include Common
11
+
12
+ option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
13
+ option ['-p', '--project-name'], 'NAME', 'Specify an alternate project name (default: directory name)'
14
+ parameter "[SERVICE] ...", "Show only specified service logs"
15
+
16
+ def execute
17
+ require_config_file(filename)
18
+
19
+ services = services_from_yaml(filename, service_list, service_prefix, true)
20
+
21
+ if services.empty? && !service_list.empty?
22
+ signal_error "No such service: #{service_list.join(', ')}"
23
+ elsif services.empty?
24
+ signal_error "No services for application"
25
+ end
26
+
27
+ query_services = services.map{|service_name, opts| prefixed_name(service_name)}.join ','
28
+ query_params = {
29
+ services: query_services,
30
+ }
31
+
32
+ show_logs("grids/#{current_grid}/container_logs", query_params) do |log|
33
+ show_log(log)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,93 @@
1
+ require_relative 'common'
2
+
3
+ module Kontena::Cli::Apps
4
+ class MonitorCommand < 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
+ show_monitor(services)
22
+ elsif !service_list.empty?
23
+ puts "No such service: #{service_list.join(', ')}".colorize(:red)
24
+ end
25
+ end
26
+
27
+ def show_monitor(services)
28
+ require_api_url
29
+ token = require_token
30
+ loop do
31
+ nodes = {}
32
+ services.each do |name, data|
33
+ service = prefixed_name(name)
34
+ result = client(token).get("services/#{current_grid}/#{service}/containers") rescue nil
35
+ if result
36
+ services[name]['instances'] = result['containers'].size
37
+ result['containers'].each do |container|
38
+ container['service'] = name
39
+ nodes[container['node']['name']] ||= []
40
+ nodes[container['node']['name']] << container
41
+ end
42
+ end
43
+ end
44
+ clear_terminal
45
+ puts "services:"
46
+ services.each do |name, data|
47
+ color = color_for_service(name)
48
+ puts " #{"■".colorize(color)} #{name} (#{data['instances']} instances)"
49
+ end
50
+ puts "nodes:"
51
+ node_names = nodes.keys.sort
52
+ node_names.each do |name|
53
+ containers = nodes[name]
54
+ puts " #{name} (#{containers.size} instances)"
55
+ print " "
56
+ containers.each do |container|
57
+ icon = "■"
58
+ if container['status'] != 'running'
59
+ icon = "□"
60
+ end
61
+ color = color_for_service(container['service'])
62
+ print icon.colorize(color)
63
+ end
64
+ puts ''
65
+ end
66
+ sleep 1
67
+ end
68
+ end
69
+
70
+ def color_for_service(service)
71
+ color_maps[service] = colors.shift unless color_maps[service]
72
+ color_maps[service].to_sym
73
+ end
74
+
75
+ def color_maps
76
+ @color_maps ||= {}
77
+ end
78
+
79
+ def colors
80
+ if(@colors.nil? || @colors.size == 0)
81
+ @colors = %i(
82
+ red green yellow blue magenta cyan bright_red bright_green
83
+ bright_yellow bright_blue bright_magenta bright_cyan
84
+ )
85
+ end
86
+ @colors
87
+ end
88
+
89
+ def clear_terminal
90
+ print "\e[H\e[2J"
91
+ end
92
+ end
93
+ end