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