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