hako 0.1.0
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 +11 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +64 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/hello-lb.yml +31 -0
- data/examples/hello.env +1 -0
- data/examples/hello.yml +22 -0
- data/exe/hako +4 -0
- data/hako.gemspec +27 -0
- data/lib/hako.rb +8 -0
- data/lib/hako/cli.rb +18 -0
- data/lib/hako/commander.rb +66 -0
- data/lib/hako/env_expander.rb +91 -0
- data/lib/hako/env_provider.rb +22 -0
- data/lib/hako/env_providers.rb +4 -0
- data/lib/hako/env_providers/file.rb +43 -0
- data/lib/hako/error.rb +4 -0
- data/lib/hako/front.rb +13 -0
- data/lib/hako/front_config.rb +27 -0
- data/lib/hako/fronts.rb +4 -0
- data/lib/hako/fronts/nginx.rb +19 -0
- data/lib/hako/scheduler.rb +36 -0
- data/lib/hako/schedulers.rb +4 -0
- data/lib/hako/schedulers/ecs.rb +310 -0
- data/lib/hako/schedulers/ecs_definition_comparator.rb +51 -0
- data/lib/hako/templates/nginx.conf.erb +13 -0
- data/lib/hako/version.rb +3 -0
- metadata +145 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA1:
         | 
| 3 | 
            +
              metadata.gz: b7b7ad2ffbeb17c94a9e690bf278bb4019852b3e
         | 
| 4 | 
            +
              data.tar.gz: 27024dcecc995a1acc893e3072029cd693fd8e0a
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 874b5c28b65dd52d728c83a441f23d057807d1f445fe9edc5a5b44200eeb9b8fbcfd9770568a86f2ecee30686dcc389e713a2baed9dfef4e7ade3c05633905f1
         | 
| 7 | 
            +
              data.tar.gz: 3218b993bcc4d924f0e6aaaf72a74dc84e2d97f07babcea01295c25625e5fcd13bcb9fad70ea254d30518e42778a14afc17d5df95cec657ddbd7d49f5909748c
         | 
    
        data/.gitignore
    ADDED
    
    
    
        data/.rspec
    ADDED
    
    
    
        data/.travis.yml
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            # Hako
         | 
| 2 | 
            +
            Deploy Docker container.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            ## Status
         | 
| 5 | 
            +
            Under development
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## Installation
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Add this line to your application's Gemfile:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ```ruby
         | 
| 12 | 
            +
            gem 'hako'
         | 
| 13 | 
            +
            ```
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            And then execute:
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                $ bundle
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            Or install it yourself as:
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                $ gem install hako
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ## Usage
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            ```
         | 
| 26 | 
            +
            % hako deploy examples/hello.yml
         | 
| 27 | 
            +
            I, [2015-10-02T12:51:24.530274 #7988]  INFO -- : Registered task-definition: arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/hello:29
         | 
| 28 | 
            +
            I, [2015-10-02T12:51:24.750501 #7988]  INFO -- : Uploaded front configuration to s3://nanika/hako/front_config/hello.conf
         | 
| 29 | 
            +
            I, [2015-10-02T12:51:24.877409 #7988]  INFO -- : Updated service: arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:service/hello
         | 
| 30 | 
            +
            I, [2015-10-02T12:56:07.284874 #7988]  INFO -- : Deployment completed
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            % hako deploy examples/hello.yml
         | 
| 33 | 
            +
            I, [2015-10-02T12:56:12.262760 #8141]  INFO -- : Deployment isn't needed
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            % hako status examples/hello.yml
         | 
| 36 | 
            +
            Load balancer:
         | 
| 37 | 
            +
              hako-hello-XXXXXXXXXX.ap-northeast-1.elb.amazonaws.com:80 -> front:80
         | 
| 38 | 
            +
            Deployments:
         | 
| 39 | 
            +
              [PRIMARY] desired_count=2, pending_count=0, running_count=2
         | 
| 40 | 
            +
            Tasks:
         | 
| 41 | 
            +
              [RUNNING]: i-XXXXXXXX (ecs-001)
         | 
| 42 | 
            +
              [RUNNING]: i-YYYYYYYY (ecs-002)
         | 
| 43 | 
            +
            Events:
         | 
| 44 | 
            +
              2015-10-05 13:35:53 +0900: (service hello) has reached a steady state.
         | 
| 45 | 
            +
              2015-10-05 13:35:14 +0900: (service hello) stopped 1 running tasks.
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            ```
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            ## Front image
         | 
| 50 | 
            +
            The front container receives these environment variables.
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            - `S3_CONFIG_BUCKET` and `S3_CONFIG_KEY`
         | 
| 53 | 
            +
                - The front container should download configuration file from S3.
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            ## Development
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            ## Contributing
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            Bug reports and pull requests are welcome on GitHub at https://github.com/eagletmt/hako.
         | 
| 64 | 
            +
             | 
    
        data/Rakefile
    ADDED
    
    
    
        data/bin/console
    ADDED
    
    | @@ -0,0 +1,14 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "bundler/setup"
         | 
| 4 | 
            +
            require "hako"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # You can add fixtures and/or initialization code here to make experimenting
         | 
| 7 | 
            +
            # with your gem easier. You can also use a different console, if you like.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # (If you use this, don't forget to add pry to your Gemfile!)
         | 
| 10 | 
            +
            # require "pry"
         | 
| 11 | 
            +
            # Pry.start
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            require "irb"
         | 
| 14 | 
            +
            IRB.start
         | 
    
        data/bin/setup
    ADDED
    
    
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            image: ryotarai/hello-sinatra
         | 
| 2 | 
            +
            env:
         | 
| 3 | 
            +
              $providers:
         | 
| 4 | 
            +
                - type: file
         | 
| 5 | 
            +
                  path: examples/hello.env
         | 
| 6 | 
            +
              PORT: 3000
         | 
| 7 | 
            +
              MESSAGE: '#{username}-san'
         | 
| 8 | 
            +
            port: 3000
         | 
| 9 | 
            +
            scheduler:
         | 
| 10 | 
            +
              type: ecs
         | 
| 11 | 
            +
              region: ap-northeast-1
         | 
| 12 | 
            +
              memory: 100
         | 
| 13 | 
            +
              cpu: 100
         | 
| 14 | 
            +
              cluster: eagletmt
         | 
| 15 | 
            +
              desired_count: 2
         | 
| 16 | 
            +
              role: ecsServiceRole
         | 
| 17 | 
            +
              elb:
         | 
| 18 | 
            +
                listeners:
         | 
| 19 | 
            +
                  - load_balancer_port: 80
         | 
| 20 | 
            +
                subnets:
         | 
| 21 | 
            +
                  - subnet-XXXXXXXX
         | 
| 22 | 
            +
                  - subnet-YYYYYYYY
         | 
| 23 | 
            +
                security_groups:
         | 
| 24 | 
            +
                  - sg-ZZZZZZZZ
         | 
| 25 | 
            +
            front:
         | 
| 26 | 
            +
              type: nginx
         | 
| 27 | 
            +
              image_tag: hako-nginx
         | 
| 28 | 
            +
              s3:
         | 
| 29 | 
            +
                region: ap-northeast-1
         | 
| 30 | 
            +
                bucket: nanika
         | 
| 31 | 
            +
                prefix: hako/front_config
         | 
    
        data/examples/hello.env
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            username=eagletmt
         | 
    
        data/examples/hello.yml
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            image: ryotarai/hello-sinatra
         | 
| 2 | 
            +
            env:
         | 
| 3 | 
            +
              $providers:
         | 
| 4 | 
            +
                - type: file
         | 
| 5 | 
            +
                  path: examples/hello.env
         | 
| 6 | 
            +
              PORT: 3000
         | 
| 7 | 
            +
              MESSAGE: '#{username}-san'
         | 
| 8 | 
            +
            port: 3000
         | 
| 9 | 
            +
            scheduler:
         | 
| 10 | 
            +
              type: ecs
         | 
| 11 | 
            +
              region: ap-northeast-1
         | 
| 12 | 
            +
              memory: 100
         | 
| 13 | 
            +
              cpu: 100
         | 
| 14 | 
            +
              cluster: eagletmt
         | 
| 15 | 
            +
              desired_count: 2
         | 
| 16 | 
            +
            front:
         | 
| 17 | 
            +
              type: nginx
         | 
| 18 | 
            +
              image_tag: hako-nginx
         | 
| 19 | 
            +
              s3:
         | 
| 20 | 
            +
                region: ap-northeast-1
         | 
| 21 | 
            +
                bucket: nanika
         | 
| 22 | 
            +
                prefix: hako/front_config
         | 
    
        data/exe/hako
    ADDED
    
    
    
        data/hako.gemspec
    ADDED
    
    | @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 2 | 
            +
            lib = File.expand_path('../lib', __FILE__)
         | 
| 3 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 4 | 
            +
            require 'hako/version'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Gem::Specification.new do |spec|
         | 
| 7 | 
            +
              spec.name          = "hako"
         | 
| 8 | 
            +
              spec.version       = Hako::VERSION
         | 
| 9 | 
            +
              spec.authors       = ["Kohei Suzuki"]
         | 
| 10 | 
            +
              spec.email         = ["eagletmt@gmail.com"]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              spec.summary       = %q{Deploy Docker container}
         | 
| 13 | 
            +
              spec.description   = %q{Deploy Docker container}
         | 
| 14 | 
            +
              spec.homepage      = "https://github.com/eagletmt/hako"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              spec.files         = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
         | 
| 17 | 
            +
              spec.bindir        = "exe"
         | 
| 18 | 
            +
              spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 19 | 
            +
              spec.require_paths = ["lib"]
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              spec.add_dependency "aws-sdk", "~> 2.1.0"
         | 
| 22 | 
            +
              spec.add_dependency "thor"
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              spec.add_development_dependency "bundler"
         | 
| 25 | 
            +
              spec.add_development_dependency "rake"
         | 
| 26 | 
            +
              spec.add_development_dependency "rspec"
         | 
| 27 | 
            +
            end
         | 
    
        data/lib/hako.rb
    ADDED
    
    
    
        data/lib/hako/cli.rb
    ADDED
    
    | @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            require 'thor'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hako
         | 
| 4 | 
            +
              class CLI < Thor
         | 
| 5 | 
            +
                desc 'deploy FILE', 'Run deployment'
         | 
| 6 | 
            +
                option :force, aliases: %w[-f], type: :boolean, default: false, desc: 'Run deployment even if nothing is changed'
         | 
| 7 | 
            +
                def deploy(yaml_path)
         | 
| 8 | 
            +
                  require 'hako/commander'
         | 
| 9 | 
            +
                  Commander.new(yaml_path).deploy(force: options[:force])
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                desc 'status FILE', 'Show deployment status'
         | 
| 13 | 
            +
                def status(yaml_path)
         | 
| 14 | 
            +
                  require 'hako/commander'
         | 
| 15 | 
            +
                  Commander.new(yaml_path).status
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -0,0 +1,66 @@ | |
| 1 | 
            +
            require 'yaml'
         | 
| 2 | 
            +
            require 'hako/env_expander'
         | 
| 3 | 
            +
            require 'hako/error'
         | 
| 4 | 
            +
            require 'hako/front_config'
         | 
| 5 | 
            +
            require 'hako/fronts'
         | 
| 6 | 
            +
            require 'hako/schedulers'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Hako
         | 
| 9 | 
            +
              class Commander
         | 
| 10 | 
            +
                PROVIDERS_KEY = '$providers'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(yaml_path)
         | 
| 13 | 
            +
                  @app_id = Pathname.new(yaml_path).basename.sub_ext('').to_s
         | 
| 14 | 
            +
                  @yaml = YAML.load_file(yaml_path)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def deploy(force: false)
         | 
| 18 | 
            +
                  env = @yaml['env'].dup
         | 
| 19 | 
            +
                  providers = load_providers(env.delete(PROVIDERS_KEY) || [])
         | 
| 20 | 
            +
                  env = EnvExpander.new(providers).expand(env)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  front = load_front(@yaml['front'])
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  scheduler = load_scheduler(@yaml['scheduler'])
         | 
| 25 | 
            +
                  app_port = @yaml.fetch('port', nil)
         | 
| 26 | 
            +
                  image_tag = @yaml['image']  # TODO: Append revision
         | 
| 27 | 
            +
                  scheduler.deploy(image_tag, env, app_port, front, force: force)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def status
         | 
| 31 | 
            +
                  load_scheduler(@yaml['scheduler']).status
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                private
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def load_providers(provider_configs)
         | 
| 37 | 
            +
                  provider_configs.map do |config|
         | 
| 38 | 
            +
                    type = config['type']
         | 
| 39 | 
            +
                    unless type
         | 
| 40 | 
            +
                      raise Error.new("type must be set in each #{PROVIDERS_KEY} element")
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                    require "hako/env_providers/#{type}"
         | 
| 43 | 
            +
                    Hako::EnvProviders.const_get(camelize(type)).new(config)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def load_scheduler(scheduler_config)
         | 
| 48 | 
            +
                  type = scheduler_config['type']
         | 
| 49 | 
            +
                  unless type
         | 
| 50 | 
            +
                    raise Error.new('type must be set in scheduler')
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                  require "hako/schedulers/#{type}"
         | 
| 53 | 
            +
                  Hako::Schedulers.const_get(camelize(type)).new(@app_id, scheduler_config)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def load_front(yaml)
         | 
| 57 | 
            +
                  front_config = FrontConfig.new(yaml)
         | 
| 58 | 
            +
                  require "hako/fronts/#{front_config.type}"
         | 
| 59 | 
            +
                  Hako::Fronts.const_get(camelize(front_config.type)).new(front_config)
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def camelize(name)
         | 
| 63 | 
            +
                  name.split('_').map(&:capitalize).join('')
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
            end
         | 
| @@ -0,0 +1,91 @@ | |
| 1 | 
            +
            require 'set'
         | 
| 2 | 
            +
            require 'strscan'
         | 
| 3 | 
            +
            require 'hako/env_providers'
         | 
| 4 | 
            +
            require 'hako/error'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Hako
         | 
| 7 | 
            +
              class EnvExpander
         | 
| 8 | 
            +
                class ExpansionError < Error
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                class Literal < Struct.new(:literal)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                class Variable < Struct.new(:name)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def initialize(providers)
         | 
| 18 | 
            +
                  @providers = providers
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def expand(env)
         | 
| 22 | 
            +
                  parsed_env = {}
         | 
| 23 | 
            +
                  variables = Set.new
         | 
| 24 | 
            +
                  env.each do |key, val|
         | 
| 25 | 
            +
                    tokens = parse(val.to_s)
         | 
| 26 | 
            +
                    tokens.each do |t|
         | 
| 27 | 
            +
                      if t.is_a?(Variable)
         | 
| 28 | 
            +
                        variables << t.name
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                    parsed_env[key] = tokens
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  values = {}
         | 
| 35 | 
            +
                  @providers.each do |provider|
         | 
| 36 | 
            +
                    if variables.empty?
         | 
| 37 | 
            +
                      break
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                    provider.ask(variables.to_a).each do |var, val|
         | 
| 40 | 
            +
                      values[var] = val
         | 
| 41 | 
            +
                      variables.delete(var)
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                  unless variables.empty?
         | 
| 45 | 
            +
                    raise ExpansionError.new("Unresolvable variables: #{variables.to_a}")
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  expanded_env = {}
         | 
| 49 | 
            +
                  parsed_env.each do |key, tokens|
         | 
| 50 | 
            +
                    expanded_env[key] = tokens.map { |t| expand_value(values, t) }.join('')
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                  expanded_env
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                private
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def parse(value)
         | 
| 58 | 
            +
                  s = StringScanner.new(value)
         | 
| 59 | 
            +
                  tokens = []
         | 
| 60 | 
            +
                  pos = 0
         | 
| 61 | 
            +
                  while s.scan_until(/#\{(.*?)\}/)
         | 
| 62 | 
            +
                    pre = s.string.byteslice(pos ... (s.pos - s.matched.size))
         | 
| 63 | 
            +
                    var = s[1]
         | 
| 64 | 
            +
                    unless pre.empty?
         | 
| 65 | 
            +
                      tokens << Literal.new(pre)
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                    if var.empty?
         | 
| 68 | 
            +
                      raise ExpansionError.new("Empty interpolation is not allowed")
         | 
| 69 | 
            +
                    else
         | 
| 70 | 
            +
                      tokens << Variable.new(var)
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                    pos = s.pos
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                  unless s.rest.empty?
         | 
| 75 | 
            +
                    tokens << Literal.new(s.rest)
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                  tokens
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def expand_value(values, token)
         | 
| 81 | 
            +
                  case token
         | 
| 82 | 
            +
                  when Literal
         | 
| 83 | 
            +
                    token.literal
         | 
| 84 | 
            +
                  when Variable
         | 
| 85 | 
            +
                    values.fetch(token.name)
         | 
| 86 | 
            +
                  else
         | 
| 87 | 
            +
                    raise ExpansionError.new("Unknown token type: #{token.class}")
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            require 'hako/error'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hako
         | 
| 4 | 
            +
              class EnvProvider
         | 
| 5 | 
            +
                class ValidationError < Error
         | 
| 6 | 
            +
                end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(_options)
         | 
| 9 | 
            +
                  raise NotImplementedError
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def ask(_variables)
         | 
| 13 | 
            +
                  raise NotImplementedError
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                private
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def validation_error!(message)
         | 
| 19 | 
            +
                  raise ValidationError.new(message)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            require 'hako/env_provider'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hako
         | 
| 4 | 
            +
              module EnvProviders
         | 
| 5 | 
            +
                class File < EnvProvider
         | 
| 6 | 
            +
                  def initialize(options)
         | 
| 7 | 
            +
                    unless options['path']
         | 
| 8 | 
            +
                      validation_error!("path must be set")
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                    @path = options['path']
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def ask(variables)
         | 
| 14 | 
            +
                    env = {}
         | 
| 15 | 
            +
                    read_from_file do |key, val|
         | 
| 16 | 
            +
                      if variables.include?(key)
         | 
| 17 | 
            +
                        env[key] = val
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                    env
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def read_from_file(&block)
         | 
| 26 | 
            +
                    ::File.open(@path) do |f|
         | 
| 27 | 
            +
                      f.each_line do |line|
         | 
| 28 | 
            +
                        line.chomp!
         | 
| 29 | 
            +
                        line.lstrip!
         | 
| 30 | 
            +
                        if line[0] == '#'
         | 
| 31 | 
            +
                          # line comment
         | 
| 32 | 
            +
                          next
         | 
| 33 | 
            +
                        end
         | 
| 34 | 
            +
                        key, val = line.split('=', 2)
         | 
| 35 | 
            +
                        if val
         | 
| 36 | 
            +
                          block.call(key, val)
         | 
| 37 | 
            +
                        end
         | 
| 38 | 
            +
                      end
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
    
        data/lib/hako/error.rb
    ADDED
    
    
    
        data/lib/hako/front.rb
    ADDED
    
    
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            require 'erb'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hako
         | 
| 4 | 
            +
              class FrontConfig < Struct.new(:type, :image_tag, :s3)
         | 
| 5 | 
            +
                class S3Config < Struct.new(:region, :bucket, :prefix)
         | 
| 6 | 
            +
                  def initialize(options)
         | 
| 7 | 
            +
                    self.region = options.fetch('region')
         | 
| 8 | 
            +
                    self.bucket = options.fetch('bucket')
         | 
| 9 | 
            +
                    self.prefix = options.fetch('prefix', nil)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def key(app_id)
         | 
| 13 | 
            +
                    if prefix
         | 
| 14 | 
            +
                      "#{prefix}/#{app_id}.conf"
         | 
| 15 | 
            +
                    else
         | 
| 16 | 
            +
                      "#{app_id}.conf"
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def initialize(options)
         | 
| 22 | 
            +
                  self.type = options.fetch('type')
         | 
| 23 | 
            +
                  self.image_tag = options.fetch('image_tag')
         | 
| 24 | 
            +
                  self.s3 = S3Config.new(options.fetch('s3'))
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
    
        data/lib/hako/fronts.rb
    ADDED
    
    
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            require 'erb'
         | 
| 2 | 
            +
            require 'hako/front'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Hako
         | 
| 5 | 
            +
              module Fronts
         | 
| 6 | 
            +
                class Nginx < Front
         | 
| 7 | 
            +
                  def generate_config(app_port)
         | 
| 8 | 
            +
                    listen_spec = "app:#{app_port}"
         | 
| 9 | 
            +
                    ERB.new(File.read(nginx_conf_erb), nil, '-').result(binding)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  private
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def nginx_conf_erb
         | 
| 15 | 
            +
                    File.expand_path('../../templates/nginx.conf.erb', __FILE__)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            require 'aws-sdk'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hako
         | 
| 4 | 
            +
              class Scheduler
         | 
| 5 | 
            +
                class ValidationError < Error
         | 
| 6 | 
            +
                end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(_app_id, _options)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def deploy(_image_tag, _env, _app_port, _front_config, _options)
         | 
| 12 | 
            +
                  raise NotImplementedError
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def status
         | 
| 16 | 
            +
                  raise NotImplementedError
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def upload_front_config(app_id, front, app_port)
         | 
| 20 | 
            +
                  front_conf = front.generate_config(app_port)
         | 
| 21 | 
            +
                  s3_config = front.config.s3
         | 
| 22 | 
            +
                  s3 = Aws::S3::Client.new(region: s3_config.region)
         | 
| 23 | 
            +
                  s3.put_object(
         | 
| 24 | 
            +
                    body: front_conf,
         | 
| 25 | 
            +
                    bucket: s3_config.bucket,
         | 
| 26 | 
            +
                    key: s3_config.key(app_id),
         | 
| 27 | 
            +
                  )
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                private
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def validation_error!(message)
         | 
| 33 | 
            +
                  raise ValidationError.new(message)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| @@ -0,0 +1,310 @@ | |
| 1 | 
            +
            require 'aws-sdk'
         | 
| 2 | 
            +
            require 'hako'
         | 
| 3 | 
            +
            require 'hako/error'
         | 
| 4 | 
            +
            require 'hako/scheduler'
         | 
| 5 | 
            +
            require 'hako/schedulers/ecs_definition_comparator'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Hako
         | 
| 8 | 
            +
              module Schedulers
         | 
| 9 | 
            +
                class Ecs < Scheduler
         | 
| 10 | 
            +
                  DEFAULT_CLUSTER = 'default'
         | 
| 11 | 
            +
                  DEFAULT_FRONT_PORT = 10000
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(app_id, options)
         | 
| 14 | 
            +
                    @app_id = app_id
         | 
| 15 | 
            +
                    @cluster = options.fetch('cluster', DEFAULT_CLUSTER)
         | 
| 16 | 
            +
                    @desired_count = options.fetch('desired_count') { validation_error!('desired_count must be set') }
         | 
| 17 | 
            +
                    @cpu = options.fetch('cpu') { validation_error!('cpu must be set') }
         | 
| 18 | 
            +
                    @memory = options.fetch('memory') { validation_error!('memory must be set') }
         | 
| 19 | 
            +
                    region = options.fetch('region') { validation_error!('region must be set') }
         | 
| 20 | 
            +
                    @role = options.fetch('role', nil)
         | 
| 21 | 
            +
                    @ecs = Aws::ECS::Client.new(region: region)
         | 
| 22 | 
            +
                    @elb = Aws::ElasticLoadBalancing::Client.new(region: region)
         | 
| 23 | 
            +
                    @ec2 = Aws::EC2::Client.new(region: region)
         | 
| 24 | 
            +
                    @elb_config = options.fetch('elb', nil)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def deploy(image_tag, env, app_port, front, force: false)
         | 
| 28 | 
            +
                    @force_mode = force
         | 
| 29 | 
            +
                    front_env = {
         | 
| 30 | 
            +
                      'AWS_DEFAULT_REGION' => front.config.s3.region,
         | 
| 31 | 
            +
                      'S3_CONFIG_BUCKET' => front.config.s3.bucket,
         | 
| 32 | 
            +
                      'S3_CONFIG_KEY' => front.config.s3.key(@app_id),
         | 
| 33 | 
            +
                    }
         | 
| 34 | 
            +
                    front_port = determine_front_port(front)
         | 
| 35 | 
            +
                    task_definition = register_task_definition(image_tag, env, front.config, front_env, front_port)
         | 
| 36 | 
            +
                    if task_definition == :noop
         | 
| 37 | 
            +
                      Hako.logger.info "Task definition isn't changed"
         | 
| 38 | 
            +
                      task_definition = @ecs.describe_task_definition(task_definition: @app_id).task_definition
         | 
| 39 | 
            +
                    else
         | 
| 40 | 
            +
                      Hako.logger.info "Registered task definition: #{task_definition.task_definition_arn}"
         | 
| 41 | 
            +
                      upload_front_config(@app_id, front, app_port)
         | 
| 42 | 
            +
                      Hako.logger.info "Uploaded front configuration to s3://#{front.config.s3.bucket}/#{front.config.s3.key(@app_id)}"
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                    service = create_or_update_service(task_definition.task_definition_arn, front_port)
         | 
| 45 | 
            +
                    if service == :noop
         | 
| 46 | 
            +
                      Hako.logger.info "Service isn't changed"
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      Hako.logger.info "Updated service: #{service.service_arn}"
         | 
| 49 | 
            +
                      wait_for_ready(service)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                    Hako.logger.info "Deployment completed"
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def status
         | 
| 55 | 
            +
                    service = @ecs.describe_services(cluster: @cluster, services: [@app_id]).services[0]
         | 
| 56 | 
            +
                    unless service
         | 
| 57 | 
            +
                      puts 'Unavailable'
         | 
| 58 | 
            +
                      exit 1
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    unless service.load_balancers.empty?
         | 
| 62 | 
            +
                      lb = service.load_balancers[0]
         | 
| 63 | 
            +
                      lb_detail = @elb.describe_load_balancers(load_balancer_names: [lb.load_balancer_name]).load_balancer_descriptions[0]
         | 
| 64 | 
            +
                      puts 'Load balancer:'
         | 
| 65 | 
            +
                      lb_detail.listener_descriptions.each do |ld|
         | 
| 66 | 
            +
                        l = ld.listener
         | 
| 67 | 
            +
                        puts "  #{lb_detail.dns_name}:#{l.load_balancer_port} -> #{lb.container_name}:#{lb.container_port}"
         | 
| 68 | 
            +
                      end
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    puts 'Deployments:'
         | 
| 72 | 
            +
                    service.deployments.each do |d|
         | 
| 73 | 
            +
                      puts "  [#{d.status}] desired_count=#{d.desired_count}, pending_count=#{d.pending_count}, running_count=#{d.running_count}"
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    puts 'Tasks:'
         | 
| 77 | 
            +
                    @ecs.list_tasks(cluster: @cluster, service_name: @app_id).each do |page|
         | 
| 78 | 
            +
                      unless page.task_arns.empty?
         | 
| 79 | 
            +
                        tasks = @ecs.describe_tasks(cluster: @cluster, tasks: page.task_arns).tasks
         | 
| 80 | 
            +
                        container_instances = {}
         | 
| 81 | 
            +
                        @ecs.describe_container_instances(cluster: @cluster, container_instances: tasks.map(&:container_instance_arn)).container_instances.each do |ci|
         | 
| 82 | 
            +
                          container_instances[ci.container_instance_arn] = ci
         | 
| 83 | 
            +
                        end
         | 
| 84 | 
            +
                        ec2_instances = {}
         | 
| 85 | 
            +
                        @ec2.describe_instances(instance_ids: container_instances.values.map(&:ec2_instance_id)).reservations.each do |r|
         | 
| 86 | 
            +
                          r.instances.each do |i|
         | 
| 87 | 
            +
                            ec2_instances[i.instance_id] = i
         | 
| 88 | 
            +
                          end
         | 
| 89 | 
            +
                        end
         | 
| 90 | 
            +
                        tasks.each do |task|
         | 
| 91 | 
            +
                          ci = container_instances[task.container_instance_arn]
         | 
| 92 | 
            +
                          instance = ec2_instances[ci.ec2_instance_id]
         | 
| 93 | 
            +
                          print "  [#{task.last_status}]: #{ci.ec2_instance_id}"
         | 
| 94 | 
            +
                          if instance
         | 
| 95 | 
            +
                            name_tag = instance.tags.find { |t| t.key == 'Name' }
         | 
| 96 | 
            +
                            if name_tag
         | 
| 97 | 
            +
                              print " (#{name_tag.value})"
         | 
| 98 | 
            +
                            end
         | 
| 99 | 
            +
                          end
         | 
| 100 | 
            +
                          puts
         | 
| 101 | 
            +
                        end
         | 
| 102 | 
            +
                      end
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    puts 'Events:'
         | 
| 106 | 
            +
                    service.events.first(10).each do |e|
         | 
| 107 | 
            +
                      puts "  #{e.created_at}: #{e.message}"
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  private
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  def determine_front_port(front)
         | 
| 114 | 
            +
                    service = @ecs.describe_services(cluster: @cluster, services: [@app_id]).services[0]
         | 
| 115 | 
            +
                    if service
         | 
| 116 | 
            +
                      find_front_port(service)
         | 
| 117 | 
            +
                    else
         | 
| 118 | 
            +
                      max_port = -1
         | 
| 119 | 
            +
                      @ecs.list_services(cluster: @cluster).each do |page|
         | 
| 120 | 
            +
                        unless page.service_arns.empty?
         | 
| 121 | 
            +
                          @ecs.describe_services(cluster: @cluster, services: page.service_arns).services.each do |service|
         | 
| 122 | 
            +
                            max_port = [max_port, find_front_port(service)].max
         | 
| 123 | 
            +
                          end
         | 
| 124 | 
            +
                        end
         | 
| 125 | 
            +
                      end
         | 
| 126 | 
            +
                      if max_port == -1
         | 
| 127 | 
            +
                        DEFAULT_FRONT_PORT
         | 
| 128 | 
            +
                      else
         | 
| 129 | 
            +
                        max_port+1
         | 
| 130 | 
            +
                      end
         | 
| 131 | 
            +
                    end
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def find_front_port(service)
         | 
| 135 | 
            +
                    task_definition = @ecs.describe_task_definition(task_definition: service.task_definition).task_definition
         | 
| 136 | 
            +
                    container_definitions = {}
         | 
| 137 | 
            +
                    task_definition.container_definitions.each do |c|
         | 
| 138 | 
            +
                      container_definitions[c.name] = c
         | 
| 139 | 
            +
                    end
         | 
| 140 | 
            +
                    container_definitions['front'].port_mappings[0].host_port
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  def task_definition_changed?(front, app)
         | 
| 144 | 
            +
                    if @force_mode
         | 
| 145 | 
            +
                      return true
         | 
| 146 | 
            +
                    end
         | 
| 147 | 
            +
                    task_definition = @ecs.describe_task_definition(task_definition: @app_id).task_definition
         | 
| 148 | 
            +
                    container_definitions = {}
         | 
| 149 | 
            +
                    task_definition.container_definitions.each do |c|
         | 
| 150 | 
            +
                      container_definitions[c.name] = c
         | 
| 151 | 
            +
                    end
         | 
| 152 | 
            +
                    different_definition?(front, container_definitions['front']) || different_definition?(app, container_definitions['app'])
         | 
| 153 | 
            +
                  rescue Aws::ECS::Errors::ClientException
         | 
| 154 | 
            +
                    # Task definition does not exist
         | 
| 155 | 
            +
                    true
         | 
| 156 | 
            +
                  end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                  def different_definition?(expected_container, actual_container)
         | 
| 159 | 
            +
                    EcsDefinitionComparator.new(expected_container).different?(actual_container)
         | 
| 160 | 
            +
                  end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  def register_task_definition(image_tag, env, front_config, front_env, front_port)
         | 
| 163 | 
            +
                    front = front_container(front_config, front_env, front_port)
         | 
| 164 | 
            +
                    app = app_container(image_tag, env)
         | 
| 165 | 
            +
                    if task_definition_changed?(front, app)
         | 
| 166 | 
            +
                      @ecs.register_task_definition(
         | 
| 167 | 
            +
                        family: @app_id,
         | 
| 168 | 
            +
                        container_definitions: [front, app],
         | 
| 169 | 
            +
                      ).task_definition
         | 
| 170 | 
            +
                    else
         | 
| 171 | 
            +
                      :noop
         | 
| 172 | 
            +
                    end
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  def front_container(front_config, env, front_port)
         | 
| 176 | 
            +
                    environment = env.map { |k, v| { name: k, value: v } }
         | 
| 177 | 
            +
                    {
         | 
| 178 | 
            +
                      name: 'front',
         | 
| 179 | 
            +
                      image: front_config.image_tag,
         | 
| 180 | 
            +
                      cpu: 100,
         | 
| 181 | 
            +
                      memory: 100,
         | 
| 182 | 
            +
                      links: ['app:app'],
         | 
| 183 | 
            +
                      port_mappings: [{container_port: 80, host_port: front_port, protocol: 'tcp'}],
         | 
| 184 | 
            +
                      essential: true,
         | 
| 185 | 
            +
                      environment: environment,
         | 
| 186 | 
            +
                    }
         | 
| 187 | 
            +
                  end
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  def app_container(image_tag, env)
         | 
| 190 | 
            +
                    environment = env.map { |k, v| { name: k, value: v } }
         | 
| 191 | 
            +
                    {
         | 
| 192 | 
            +
                      name: 'app',
         | 
| 193 | 
            +
                      image: image_tag,
         | 
| 194 | 
            +
                      cpu: @cpu,
         | 
| 195 | 
            +
                      memory: @memory,
         | 
| 196 | 
            +
                      links: [],
         | 
| 197 | 
            +
                      port_mappings: [],
         | 
| 198 | 
            +
                      essential: true,
         | 
| 199 | 
            +
                      environment: environment,
         | 
| 200 | 
            +
                    }
         | 
| 201 | 
            +
                  end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                  def create_or_update_service(task_definition_arn, front_port)
         | 
| 204 | 
            +
                    services = @ecs.describe_services(cluster: @cluster, services: [@app_id]).services
         | 
| 205 | 
            +
                    if services.empty?
         | 
| 206 | 
            +
                      params = {
         | 
| 207 | 
            +
                        cluster: @cluster,
         | 
| 208 | 
            +
                        service_name: @app_id,
         | 
| 209 | 
            +
                        task_definition: task_definition_arn,
         | 
| 210 | 
            +
                        desired_count: @desired_count,
         | 
| 211 | 
            +
                        role: @role,
         | 
| 212 | 
            +
                      }
         | 
| 213 | 
            +
                      if @elb_config
         | 
| 214 | 
            +
                        name = find_or_create_load_balancer(front_port)
         | 
| 215 | 
            +
                        params.merge!(
         | 
| 216 | 
            +
                          load_balancers: [
         | 
| 217 | 
            +
                            {
         | 
| 218 | 
            +
                              load_balancer_name: name,
         | 
| 219 | 
            +
                              container_name: 'front',
         | 
| 220 | 
            +
                              container_port: 80,
         | 
| 221 | 
            +
                            },
         | 
| 222 | 
            +
                          ],
         | 
| 223 | 
            +
                        )
         | 
| 224 | 
            +
                      end
         | 
| 225 | 
            +
                      @ecs.create_service(params).service
         | 
| 226 | 
            +
                    else
         | 
| 227 | 
            +
                      service = services[0]
         | 
| 228 | 
            +
                      if service.status != 'ACTIVE'
         | 
| 229 | 
            +
                        raise Error.new("Service #{service.service_arn} is already exist but the status is #{service.status}")
         | 
| 230 | 
            +
                      end
         | 
| 231 | 
            +
                      params = {
         | 
| 232 | 
            +
                        cluster: @cluster,
         | 
| 233 | 
            +
                        service: @app_id,
         | 
| 234 | 
            +
                        desired_count: @desired_count,
         | 
| 235 | 
            +
                        task_definition: task_definition_arn,
         | 
| 236 | 
            +
                      }
         | 
| 237 | 
            +
                      if service_changed?(service, params)
         | 
| 238 | 
            +
                        @ecs.update_service(params).service
         | 
| 239 | 
            +
                      else
         | 
| 240 | 
            +
                        :noop
         | 
| 241 | 
            +
                      end
         | 
| 242 | 
            +
                    end
         | 
| 243 | 
            +
                  end
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                  SERVICE_KEYS = %i[desired_count task_definition]
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                  def service_changed?(service, params)
         | 
| 248 | 
            +
                    SERVICE_KEYS.each do |key|
         | 
| 249 | 
            +
                      if service.public_send(key) != params[key]
         | 
| 250 | 
            +
                        return true
         | 
| 251 | 
            +
                      end
         | 
| 252 | 
            +
                    end
         | 
| 253 | 
            +
                    false
         | 
| 254 | 
            +
                  end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                  def wait_for_ready(service)
         | 
| 257 | 
            +
                    latest_event_id = service.events[0].id
         | 
| 258 | 
            +
                    loop do
         | 
| 259 | 
            +
                      s = @ecs.describe_services(cluster: service.cluster_arn, services: [service.service_arn]).services[0]
         | 
| 260 | 
            +
                      s.events.each do |e|
         | 
| 261 | 
            +
                        if e.id == latest_event_id
         | 
| 262 | 
            +
                          break
         | 
| 263 | 
            +
                        end
         | 
| 264 | 
            +
                        Hako.logger.info "#{e.created_at}: #{e.message}"
         | 
| 265 | 
            +
                      end
         | 
| 266 | 
            +
                      latest_event_id = s.events[0].id
         | 
| 267 | 
            +
                      finished = s.deployments.all? { |d| d.status != 'ACTIVE' }
         | 
| 268 | 
            +
                      if finished
         | 
| 269 | 
            +
                        return
         | 
| 270 | 
            +
                      else
         | 
| 271 | 
            +
                        sleep 1
         | 
| 272 | 
            +
                      end
         | 
| 273 | 
            +
                    end
         | 
| 274 | 
            +
                  end
         | 
| 275 | 
            +
             | 
| 276 | 
            +
                  def find_or_create_load_balancer(front_port)
         | 
| 277 | 
            +
                    unless load_balancer_exist?(elb_name)
         | 
| 278 | 
            +
                      listeners = @elb_config.fetch('listeners').map do |l|
         | 
| 279 | 
            +
                        {
         | 
| 280 | 
            +
                          protocol: 'tcp',
         | 
| 281 | 
            +
                          load_balancer_port: l.fetch('load_balancer_port'),
         | 
| 282 | 
            +
                          instance_port: front_port,
         | 
| 283 | 
            +
                          ssl_certificate_id: l.fetch('ssl_certificate_id', nil),
         | 
| 284 | 
            +
                        }
         | 
| 285 | 
            +
                      end
         | 
| 286 | 
            +
                      lb = @elb.create_load_balancer(
         | 
| 287 | 
            +
                        load_balancer_name: elb_name,
         | 
| 288 | 
            +
                        listeners: listeners,
         | 
| 289 | 
            +
                        subnets: @elb_config.fetch('subnets'),
         | 
| 290 | 
            +
                        security_groups: @elb_config.fetch('security_groups'),
         | 
| 291 | 
            +
                        tags: @elb_config.fetch('tags', {}).map { |k, v| { key: k, value: v.to_s } },
         | 
| 292 | 
            +
                      )
         | 
| 293 | 
            +
                      Hako.logger.info "Created ELB #{lb.dns_name} with instance_port=#{front_port}"
         | 
| 294 | 
            +
                    end
         | 
| 295 | 
            +
                    elb_name
         | 
| 296 | 
            +
                  end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                  def load_balancer_exist?(name)
         | 
| 299 | 
            +
                    @elb.describe_load_balancers(load_balancer_names: [elb_name])
         | 
| 300 | 
            +
                    true
         | 
| 301 | 
            +
                  rescue Aws::ElasticLoadBalancing::Errors::LoadBalancerNotFound
         | 
| 302 | 
            +
                    false
         | 
| 303 | 
            +
                  end
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                  def elb_name
         | 
| 306 | 
            +
                    "hako-#{@app_id}"
         | 
| 307 | 
            +
                  end
         | 
| 308 | 
            +
                end
         | 
| 309 | 
            +
              end
         | 
| 310 | 
            +
            end
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            module Hako
         | 
| 2 | 
            +
              module Schedulers
         | 
| 3 | 
            +
                class EcsDefinitionComparator
         | 
| 4 | 
            +
                  def initialize(expected_container)
         | 
| 5 | 
            +
                    @expected_container = expected_container
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  CONTAINER_KEYS = %i[image cpu memory links]
         | 
| 9 | 
            +
                  PORT_MAPPING_KEYS = %i[container_port host_port protocol]
         | 
| 10 | 
            +
                  ENVIRONMENT_KEYS = %i[name value]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def different?(actual_container)
         | 
| 13 | 
            +
                    unless actual_container
         | 
| 14 | 
            +
                      return true
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                    if different_members?(@expected_container, actual_container, CONTAINER_KEYS)
         | 
| 17 | 
            +
                      return true
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                    if @expected_container[:port_mappings].size != actual_container.port_mappings.size
         | 
| 20 | 
            +
                      return true
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                    @expected_container[:port_mappings].zip(actual_container.port_mappings) do |e, a|
         | 
| 23 | 
            +
                      if different_members?(e, a, PORT_MAPPING_KEYS)
         | 
| 24 | 
            +
                        return true
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                    if @expected_container[:environment].size != actual_container.environment.size
         | 
| 28 | 
            +
                      return true
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                    @expected_container[:environment].zip(actual_container.environment) do |e, a|
         | 
| 31 | 
            +
                      if different_members?(e, a, ENVIRONMENT_KEYS)
         | 
| 32 | 
            +
                        return true
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    false
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def different_members?(expected, actual, keys)
         | 
| 42 | 
            +
                    keys.each do |key|
         | 
| 43 | 
            +
                      if actual.public_send(key) != expected[key]
         | 
| 44 | 
            +
                        return true
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                    false
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            server {
         | 
| 2 | 
            +
              listen 80;
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              location / {
         | 
| 5 | 
            +
                proxy_pass http://<%= listen_spec %>;
         | 
| 6 | 
            +
                proxy_set_header Host $host;
         | 
| 7 | 
            +
                proxy_set_header Connection ""; # for upstream keepalive
         | 
| 8 | 
            +
                proxy_http_version 1.1;         # for upstream keepalive
         | 
| 9 | 
            +
                proxy_connect_timeout 5s;
         | 
| 10 | 
            +
                proxy_send_timeout 20s;
         | 
| 11 | 
            +
                proxy_read_timeout 20s;
         | 
| 12 | 
            +
              }
         | 
| 13 | 
            +
            }
         | 
    
        data/lib/hako/version.rb
    ADDED
    
    
    
        metadata
    ADDED
    
    | @@ -0,0 +1,145 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: hako
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Kohei Suzuki
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: exe
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2015-10-05 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: aws-sdk
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 2.1.0
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - "~>"
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 2.1.0
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: thor
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - ">="
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '0'
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - ">="
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '0'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: bundler
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :development
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: rake
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ">="
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
              type: :development
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - ">="
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: rspec
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - ">="
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '0'
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '0'
         | 
| 83 | 
            +
            description: Deploy Docker container
         | 
| 84 | 
            +
            email:
         | 
| 85 | 
            +
            - eagletmt@gmail.com
         | 
| 86 | 
            +
            executables:
         | 
| 87 | 
            +
            - hako
         | 
| 88 | 
            +
            extensions: []
         | 
| 89 | 
            +
            extra_rdoc_files: []
         | 
| 90 | 
            +
            files:
         | 
| 91 | 
            +
            - ".gitignore"
         | 
| 92 | 
            +
            - ".rspec"
         | 
| 93 | 
            +
            - ".travis.yml"
         | 
| 94 | 
            +
            - Gemfile
         | 
| 95 | 
            +
            - README.md
         | 
| 96 | 
            +
            - Rakefile
         | 
| 97 | 
            +
            - bin/console
         | 
| 98 | 
            +
            - bin/setup
         | 
| 99 | 
            +
            - examples/hello-lb.yml
         | 
| 100 | 
            +
            - examples/hello.env
         | 
| 101 | 
            +
            - examples/hello.yml
         | 
| 102 | 
            +
            - exe/hako
         | 
| 103 | 
            +
            - hako.gemspec
         | 
| 104 | 
            +
            - lib/hako.rb
         | 
| 105 | 
            +
            - lib/hako/cli.rb
         | 
| 106 | 
            +
            - lib/hako/commander.rb
         | 
| 107 | 
            +
            - lib/hako/env_expander.rb
         | 
| 108 | 
            +
            - lib/hako/env_provider.rb
         | 
| 109 | 
            +
            - lib/hako/env_providers.rb
         | 
| 110 | 
            +
            - lib/hako/env_providers/file.rb
         | 
| 111 | 
            +
            - lib/hako/error.rb
         | 
| 112 | 
            +
            - lib/hako/front.rb
         | 
| 113 | 
            +
            - lib/hako/front_config.rb
         | 
| 114 | 
            +
            - lib/hako/fronts.rb
         | 
| 115 | 
            +
            - lib/hako/fronts/nginx.rb
         | 
| 116 | 
            +
            - lib/hako/scheduler.rb
         | 
| 117 | 
            +
            - lib/hako/schedulers.rb
         | 
| 118 | 
            +
            - lib/hako/schedulers/ecs.rb
         | 
| 119 | 
            +
            - lib/hako/schedulers/ecs_definition_comparator.rb
         | 
| 120 | 
            +
            - lib/hako/templates/nginx.conf.erb
         | 
| 121 | 
            +
            - lib/hako/version.rb
         | 
| 122 | 
            +
            homepage: https://github.com/eagletmt/hako
         | 
| 123 | 
            +
            licenses: []
         | 
| 124 | 
            +
            metadata: {}
         | 
| 125 | 
            +
            post_install_message: 
         | 
| 126 | 
            +
            rdoc_options: []
         | 
| 127 | 
            +
            require_paths:
         | 
| 128 | 
            +
            - lib
         | 
| 129 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 130 | 
            +
              requirements:
         | 
| 131 | 
            +
              - - ">="
         | 
| 132 | 
            +
                - !ruby/object:Gem::Version
         | 
| 133 | 
            +
                  version: '0'
         | 
| 134 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 135 | 
            +
              requirements:
         | 
| 136 | 
            +
              - - ">="
         | 
| 137 | 
            +
                - !ruby/object:Gem::Version
         | 
| 138 | 
            +
                  version: '0'
         | 
| 139 | 
            +
            requirements: []
         | 
| 140 | 
            +
            rubyforge_project: 
         | 
| 141 | 
            +
            rubygems_version: 2.4.5.1
         | 
| 142 | 
            +
            signing_key: 
         | 
| 143 | 
            +
            specification_version: 4
         | 
| 144 | 
            +
            summary: Deploy Docker container
         | 
| 145 | 
            +
            test_files: []
         |