├── .yardopts ├── examples ├── hello.env ├── create_aws_cloud_watch_logs_log_group.libsonnet ├── front.libsonnet ├── put-ecs-container-status-to-s3 │ ├── package.json │ └── index.js ├── hello-privileged-app.jsonnet ├── hello-cap-add-app.jsonnet ├── hello-autoscaling-group.jsonnet ├── hello-awslogs-driver.jsonnet ├── hello-fargate-batch.jsonnet ├── hello-lb.jsonnet ├── hello-internal-nlb.jsonnet ├── hello-service-discovery.jsonnet ├── hello.jsonnet ├── hello-shared-alb.jsonnet ├── hello-nofront.jsonnet ├── hello-grpc.jsonnet ├── hello-lb-v2.jsonnet ├── hello-fargate.jsonnet └── hello-autoscaling.jsonnet ├── .rspec ├── spec ├── fixtures │ ├── hello.env │ ├── yaml │ │ ├── app │ │ │ └── type.yml │ │ ├── base.yml │ │ ├── scheduler │ │ │ └── type.yml │ │ ├── simple.yml │ │ ├── include_after.yml │ │ ├── include_before.yml │ │ ├── include.yml │ │ └── shovel.yml │ ├── env.yml │ └── jsonnet │ │ ├── default.jsonnet │ │ ├── ecs.jsonnet │ │ ├── default_with_links.jsonnet │ │ ├── default_with_volumes_from.jsonnet │ │ ├── parameter_store.jsonnet │ │ ├── ecs-service-discovery.jsonnet │ │ ├── ecs-elbv2.jsonnet │ │ ├── secretsmanager.jsonnet │ │ └── default_with_depends_on.jsonnet ├── hako │ ├── env_providers │ │ ├── file_spec.rb │ │ └── yaml_spec.rb │ ├── schema │ │ ├── ordered_array_spec.rb │ │ ├── unordered_array_spec.rb │ │ ├── table_spec.rb │ │ ├── nullable_spec.rb │ │ ├── structure_spec.rb │ │ └── with_default_spec.rb │ ├── schedulers │ │ ├── ecs_service_discovery_service_comparator_spec.rb │ │ ├── ecs_volume_comparator_spec.rb │ │ ├── ecs_service_comparator_spec.rb │ │ └── ecs_definition_comparator_spec.rb │ ├── yaml_loader_spec.rb │ ├── env_expander_spec.rb │ ├── scripts │ │ ├── create_aws_cloud_watch_logs_log_group_spec.rb │ │ └── nginx_front_spec.rb │ └── definition_loader_spec.rb └── spec_helper.rb ├── lib ├── hako │ ├── version.rb │ ├── scripts.rb │ ├── schedulers.rb │ ├── env_providers.rb │ ├── error.rb │ ├── schema │ │ ├── integer.rb │ │ ├── string.rb │ │ ├── boolean.rb │ │ ├── nullable.rb │ │ ├── with_default.rb │ │ ├── ordered_array.rb │ │ ├── unordered_array.rb │ │ ├── structure.rb │ │ └── table.rb │ ├── app_container.rb │ ├── templates │ │ ├── nginx.conf.erb │ │ └── nginx.location.conf.erb │ ├── schema.rb │ ├── env_provider.rb │ ├── loader.rb │ ├── application.rb │ ├── scheduler.rb │ ├── schedulers │ │ ├── ecs_service_discovery_service_comparator.rb │ │ ├── ecs_volume_comparator.rb │ │ ├── ecs_service_comparator.rb │ │ ├── ecs_elb.rb │ │ ├── ecs_definition_comparator.rb │ │ ├── ecs_autoscaling.rb │ │ ├── ecs_service_discovery.rb │ │ └── ecs_elb_v2.rb │ ├── yaml_loader.rb │ ├── script.rb │ ├── env_providers │ │ ├── file.rb │ │ └── yaml.rb │ ├── jsonnet_loader.rb │ ├── scripts │ │ ├── create_aws_cloud_watch_logs_log_group.rb │ │ └── nginx_front.rb │ ├── definition_loader.rb │ ├── env_expander.rb │ ├── commander.rb │ ├── container.rb │ └── cli.rb └── hako.rb ├── exe └── hako ├── Gemfile ├── bin ├── setup └── console ├── .gitignore ├── Rakefile ├── .rubocop_todo.yml ├── .github └── workflows │ └── ci.yml ├── LICENSE.txt ├── .rubocop.yml ├── docs ├── jsonnet.md └── ecs-task-notification.md ├── hako.gemspec ├── README.md └── CHANGELOG.md /.yardopts: -------------------------------------------------------------------------------- 1 | --private 2 | -------------------------------------------------------------------------------- /examples/hello.env: -------------------------------------------------------------------------------- 1 | username=eagletmt 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /spec/fixtures/hello.env: -------------------------------------------------------------------------------- 1 | username=eagletmt 2 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/app/type.yml: -------------------------------------------------------------------------------- 1 | image: nginx 2 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/base.yml: -------------------------------------------------------------------------------- 1 | foo: 1 2 | bar: 2 3 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/scheduler/type.yml: -------------------------------------------------------------------------------- 1 | type: ecs 2 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/simple.yml: -------------------------------------------------------------------------------- 1 | scheduler: 2 | type: ecs 3 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/include_after.yml: -------------------------------------------------------------------------------- 1 | bar: 3 2 | baz: 4 3 | <<: !include base.yml 4 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/include_before.yml: -------------------------------------------------------------------------------- 1 | <<: !include base.yml 2 | bar: 3 3 | baz: 4 4 | -------------------------------------------------------------------------------- /lib/hako/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | VERSION = '2.17.0' 5 | end 6 | -------------------------------------------------------------------------------- /examples/create_aws_cloud_watch_logs_log_group.libsonnet: -------------------------------------------------------------------------------- 1 | { type: 'create_aws_cloud_watch_logs_log_group' } 2 | -------------------------------------------------------------------------------- /lib/hako/scripts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Scripts 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/include.yml: -------------------------------------------------------------------------------- 1 | scheduler: 2 | !include scheduler/type.yml 3 | app: 4 | !include app/type.yml 5 | -------------------------------------------------------------------------------- /lib/hako/schedulers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schedulers 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /exe/hako: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'hako/cli' 5 | 6 | Hako::CLI.start(ARGV) 7 | -------------------------------------------------------------------------------- /lib/hako/env_providers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module EnvProviders 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/hako/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | class Error < StandardError 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/yaml/shovel.yml: -------------------------------------------------------------------------------- 1 | scheduler: 2 | <<: !include scheduler/type.yml 3 | desired_count: 1 4 | app: 5 | !include app/type.yml 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in hako.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | spec/examples.txt 12 | -------------------------------------------------------------------------------- /examples/front.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | type: 'nginx_front', 3 | s3: { 4 | region: 'ap-northeast-1', 5 | bucket: 'nanika', 6 | prefix: 'hako/front_config', 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /examples/put-ecs-container-status-to-s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "put-ecs-container-status-to-s3", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/env.yml: -------------------------------------------------------------------------------- 1 | username: eagletmt 2 | host: 3 | - app-001 4 | - app-002 5 | app: 6 | db: 7 | host: db-001 8 | port: 3306 9 | cache: 10 | host: cache-001 11 | port: 11211 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/default.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | app: { 3 | image: 'app-image', 4 | }, 5 | sidecars: { 6 | front: { 7 | type: 'nginx', 8 | image_tag: 'front-image', 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /lib/hako/schema/integer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class Integer 6 | def valid?(object) 7 | object.is_a?(::Integer) 8 | end 9 | 10 | def same?(x, y) 11 | x == y 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/hako/schema/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class String 6 | def valid?(object) 7 | object.is_a?(::String) 8 | end 9 | 10 | def same?(x, y) 11 | x == y 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/hako/app_container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hako/container' 4 | 5 | module Hako 6 | class AppContainer < Container 7 | # @return [String] 8 | def image_tag 9 | "#{@definition['image']}:#{@definition.fetch('tag', 'latest')}" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/ecs.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | role: 'ECSServiceRole', 8 | }, 9 | app: { 10 | image: 'busybox', 11 | cpu: 32, 12 | memory: 64, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | require 'yard' 7 | 8 | RSpec::Core::RakeTask.new(:spec) 9 | RuboCop::RakeTask.new(:rubocop) 10 | YARD::Rake::YardocTask.new(:yard) 11 | 12 | task :default => %i[spec rubocop] 13 | -------------------------------------------------------------------------------- /lib/hako/schema/boolean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class Boolean 6 | def valid?(object) 7 | object.is_a?(FalseClass) || object.is_a?(TrueClass) 8 | end 9 | 10 | def same?(x, y) 11 | x == y 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/hako.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | require 'hako/version' 5 | 6 | module Hako 7 | def self.logger 8 | @logger ||= 9 | begin 10 | $stdout.sync = true 11 | Logger.new($stdout).tap do |l| 12 | l.level = Logger::INFO 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/hako/templates/nginx.conf.erb: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | <%- if client_max_body_size -%> 5 | client_max_body_size <%= client_max_body_size %>; 6 | <%- end -%> 7 | 8 | <%- locations.each do |path, location| -%> 9 | location <%= path %> { 10 | <%= render_location(listen_spec, location) %> 11 | } 12 | <%- end -%> 13 | } 14 | -------------------------------------------------------------------------------- /examples/hello-privileged-app.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | }, 8 | app: { 9 | image: 'busybox', 10 | memory: 128, 11 | cpu: 256, 12 | command: ['echo', 'hello from --privileged mode'], 13 | privileged: true, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /lib/hako/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hako/schema/boolean' 4 | require 'hako/schema/integer' 5 | require 'hako/schema/nullable' 6 | require 'hako/schema/ordered_array' 7 | require 'hako/schema/string' 8 | require 'hako/schema/structure' 9 | require 'hako/schema/table' 10 | require 'hako/schema/unordered_array' 11 | require 'hako/schema/with_default' 12 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/default_with_links.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | app: { 3 | image: 'app-image', 4 | links: ['redis'], 5 | }, 6 | sidecars: { 7 | redis: { 8 | image_tag: 'redis', 9 | links: ['memcached'], 10 | }, 11 | memcached: { 12 | image_tag: 'memcached', 13 | }, 14 | fluentd: { 15 | image_tag: 'fluentd', 16 | links: ['redis'], 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'hako' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start 16 | -------------------------------------------------------------------------------- /examples/hello-cap-add-app.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | }, 8 | app: { 9 | image: 'busybox', 10 | memory: 128, 11 | cpu: 256, 12 | command: ['echo', 'hello with SYS_RAWIO'], 13 | linux_parameters: { 14 | capabilities: { 15 | add: ['SYS_RAWIO'], 16 | }, 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /lib/hako/templates/nginx.location.conf.erb: -------------------------------------------------------------------------------- 1 | <%- if location.allow_only_from -%> 2 | <%- location.allow_only_from.each do |ip| -%> 3 | allow <%= ip %>; 4 | <%- end -%> 5 | deny all; 6 | <%- end -%> 7 | 8 | proxy_pass http://<%= listen_spec %>; 9 | proxy_set_header Host $host; 10 | proxy_set_header Connection ""; # for upstream keepalive 11 | proxy_http_version 1.1; # for upstream keepalive 12 | proxy_connect_timeout 5s; 13 | proxy_send_timeout 20s; 14 | proxy_read_timeout 20s; 15 | -------------------------------------------------------------------------------- /examples/hello-autoscaling-group.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 2, 7 | role: 'ecsServiceRole', 8 | autoscaling_group_for_oneshot: 'hako-batch-cluster', 9 | }, 10 | app: { 11 | image: 'ryotarai/hello-sinatra', 12 | memory: 128, 13 | cpu: 256, 14 | env: { 15 | PORT: '3000', 16 | MESSAGE: 'hello', 17 | }, 18 | command: ['echo', 'heavy offline job'], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | Metrics/AbcSize: 2 | Enabled: false 3 | Metrics/BlockLength: 4 | Enabled: false 5 | Metrics/ClassLength: 6 | Enabled: false 7 | Metrics/CyclomaticComplexity: 8 | Enabled: false 9 | Metrics/MethodLength: 10 | Enabled: false 11 | Metrics/ParameterLists: 12 | Enabled: false 13 | Metrics/PerceivedComplexity: 14 | Enabled: false 15 | Metrics/BlockNesting: 16 | Enabled: false 17 | 18 | Style/Documentation: 19 | Enabled: false 20 | Style/SafeNavigation: 21 | # extremely buggy in v0.43.0 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /lib/hako/schema/nullable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class Nullable 6 | def initialize(schema) 7 | @schema = schema 8 | end 9 | 10 | def valid?(object) 11 | object.nil? || @schema.valid?(object) 12 | end 13 | 14 | def same?(x, y) 15 | if x.nil? && y.nil? 16 | true 17 | elsif x.nil? || y.nil? 18 | false 19 | else 20 | @schema.same?(x, y) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hako/schema/with_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class WithDefault 6 | def initialize(schema, default) 7 | @schema = schema 8 | @default = default 9 | end 10 | 11 | def valid?(object) 12 | object.nil? || @schema.valid?(object) 13 | end 14 | 15 | def same?(x, y) 16 | @schema.same?(wrap(x), wrap(y)) 17 | end 18 | 19 | private 20 | 21 | def wrap(x) 22 | x.nil? ? @default : x 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/default_with_volumes_from.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | app: { 3 | image: 'app-image', 4 | volumes_from: [ 5 | { source_container: 'redis' }, 6 | ], 7 | }, 8 | sidecars: { 9 | redis: { 10 | image_tag: 'redis', 11 | volumes_from: [ 12 | { source_container: 'memcached' }, 13 | ], 14 | }, 15 | memcached: { 16 | image_tag: 'memcached', 17 | }, 18 | fluentd: { 19 | image_tag: 'fluentd', 20 | volumes_from: [ 21 | { source_container: 'redis' }, 22 | ], 23 | }, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /lib/hako/schema/ordered_array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class OrderedArray 6 | def initialize(schema) 7 | @schema = schema 8 | end 9 | 10 | def valid?(object) 11 | object.is_a?(Array) && object.all? { |e| @schema.valid?(e) } 12 | end 13 | 14 | def same?(xs, ys) 15 | if xs.size != ys.size 16 | return false 17 | end 18 | 19 | xs.zip(ys) do |x, y| 20 | unless @schema.same?(x, y) 21 | return false 22 | end 23 | end 24 | true 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /examples/hello-awslogs-driver.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | }, 8 | app: { 9 | image: 'busybox', 10 | memory: 128, 11 | cpu: 256, 12 | command: ['echo', 'hello awslogs'], 13 | log_configuration: { 14 | log_driver: 'awslogs', 15 | options: { 16 | 'awslogs-group': 'my-logs', 17 | 'awslogs-region': 'ap-northeast-1', 18 | 'awslogs-stream-prefix': 'example', 19 | }, 20 | }, 21 | }, 22 | scripts: [ 23 | (import 'create_aws_cloud_watch_logs_log_group.libsonnet'), 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /lib/hako/env_provider.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hako/error' 4 | 5 | module Hako 6 | class EnvProvider 7 | class ValidationError < Error 8 | end 9 | 10 | def initialize(_root_path, _options) 11 | raise NotImplementedError 12 | end 13 | 14 | def ask(_variables) 15 | raise NotImplementedError 16 | end 17 | 18 | def can_ask_keys? 19 | raise NotImplementedError 20 | end 21 | 22 | def ask_keys(_variables) 23 | raise NotImplementedError 24 | end 25 | 26 | private 27 | 28 | def validation_error!(message) 29 | raise ValidationError.new(message) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/hako/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | class Loader 5 | # @param [Module] base_module 6 | # @param [String] base_path 7 | def initialize(base_module, base_path) 8 | @base_module = base_module 9 | @base_path = base_path 10 | end 11 | 12 | # @param [String] name 13 | # @return [Module] 14 | def load(name) 15 | require "#{@base_path}/#{name}" 16 | @base_module.const_get(camelize(name)) 17 | end 18 | 19 | private 20 | 21 | # @param [String] name 22 | # @return [String] 23 | def camelize(name) 24 | name.split('_').map(&:capitalize).join('') 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/hako/schema/unordered_array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class UnorderedArray 6 | def initialize(schema) 7 | @schema = schema 8 | end 9 | 10 | def valid?(object) 11 | object.is_a?(Array) && object.all? { |e| @schema.valid?(e) } 12 | end 13 | 14 | def same?(xs, ys) 15 | if xs.size != ys.size 16 | return false 17 | end 18 | 19 | t = xs.dup 20 | ys.each do |y| 21 | i = t.index { |x| @schema.same?(x, y) } 22 | if i 23 | t.delete_at(i) 24 | else 25 | return false 26 | end 27 | end 28 | 29 | true 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/hako/env_providers/file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/env_providers/file' 5 | 6 | RSpec.describe Hako::EnvProviders::File do 7 | let(:provider) { described_class.new(fixture_root, options) } 8 | let(:options) { { 'path' => 'hello.env' } } 9 | 10 | describe '#ask' do 11 | it 'returns known variables' do 12 | expect(provider.ask(['username'])).to eq('username' => 'eagletmt') 13 | end 14 | 15 | it 'returns empty to unknown variables' do 16 | expect(provider.ask(['undefined'])).to eq({}) 17 | end 18 | end 19 | 20 | describe '#ask_keys' do 21 | it 'returns known variables' do 22 | expect(provider.ask_keys(%w[username undefined])).to match_array(['username']) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/parameter_store.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | role: 'ECSServiceRole', 8 | }, 9 | app: { 10 | image: 'busybox', 11 | cpu: 32, 12 | memory: 64, 13 | secrets: [ 14 | { 15 | name: 'SECRET_MESSAGE1', 16 | value_from: 'arn:aws:ssm:ap-northeast-1:012345678901:parameter/hoge/fuga/SECRET_MESSAGE1', 17 | }, 18 | { 19 | name: 'SECRET_MESSAGE2', 20 | value_from: 'arn:aws:ssm:ap-northeast-1:012345678901:parameter/hoge/fuga/SECRET_MESSAGE2', 21 | }, 22 | { 23 | name: 'SECRET_MESSAGE3', 24 | value_from: 'arn:aws:ssm:us-east-1:012345678901:parameter/hoge/fuga/SECRET_MESSAGE3', 25 | }, 26 | ], 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /lib/hako/schema/structure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class Structure 6 | def initialize 7 | @members = {} 8 | end 9 | 10 | def valid?(object) 11 | unless object.is_a?(::Hash) 12 | return false 13 | end 14 | 15 | @members.each do |key, val_schema| 16 | unless val_schema.valid?(object[key]) 17 | return false 18 | end 19 | end 20 | true 21 | end 22 | 23 | def same?(x, y) 24 | @members.each do |key, val_schema| 25 | unless val_schema.same?(x[key], y[key]) 26 | return false 27 | end 28 | end 29 | true 30 | end 31 | 32 | def member(key, schema) 33 | @members[key] = schema 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/hako/schema/table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | module Schema 5 | class Table 6 | def initialize(key_schema, val_schema) 7 | @key_schema = key_schema 8 | @val_schema = val_schema 9 | end 10 | 11 | def valid?(object) 12 | object.is_a?(::Hash) && object.all? { |k, v| @key_schema.valid?(k) && @val_schema.valid?(v) } 13 | end 14 | 15 | def same?(xs, ys) 16 | if xs.size != ys.size 17 | return false 18 | end 19 | 20 | t = xs.dup 21 | ys.each do |yk, yv| 22 | xk, = xs.find { |k, v| @key_schema.same?(k, yk) && @val_schema.same?(v, yv) } 23 | if xk 24 | t.delete(xk) 25 | else 26 | return false 27 | end 28 | end 29 | 30 | t.empty? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/ecs-service-discovery.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | role: 'ECSServiceRole', 8 | service_discovery: [ 9 | { 10 | container_name: 'app', 11 | container_port: 80, 12 | service: { 13 | name: 'ecs-service-discovery', 14 | namespace_id: 'ns-1111111111111111', 15 | dns_config: { 16 | dns_records: [ 17 | { 18 | type: 'SRV', 19 | ttl: 60, 20 | }, 21 | ], 22 | }, 23 | health_check_custom_config: { 24 | failure_threshold: 1, 25 | }, 26 | }, 27 | }, 28 | ], 29 | }, 30 | app: { 31 | image: 'busybox', 32 | cpu: 32, 33 | memory: 64, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | pull_request: 7 | 8 | jobs: 9 | rspec: 10 | name: RSpec 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: 16 | - '3.1' 17 | - '3.2' 18 | - '3.3' 19 | - '3.4' 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - run: bundle exec rspec 27 | rubocop: 28 | name: RuboCop 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: ruby/setup-ruby@v1 33 | with: 34 | # Use the same version with .rubocop.yml 35 | ruby-version: 3.1 36 | bundler-cache: true 37 | - run: bundle exec rubocop 38 | -------------------------------------------------------------------------------- /spec/hako/schema/ordered_array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/schema/integer' 5 | require 'hako/schema/ordered_array' 6 | 7 | RSpec.describe Hako::Schema::OrderedArray do 8 | let(:schema) { described_class.new(subschema) } 9 | let(:subschema) { Hako::Schema::Integer.new } 10 | 11 | describe '#valid?' do 12 | it do 13 | expect(schema).to be_valid([1, 2, 3]) 14 | expect(schema).to_not be_valid(nil) 15 | expect(schema).to_not be_valid([1, nil]) 16 | expect(schema).to_not be_valid([1, '2']) 17 | end 18 | end 19 | 20 | describe '#same?' do 21 | it do 22 | expect(schema).to be_same([1, 2, 3], [1, 2, 3]) 23 | expect(schema).to_not be_same([1, 2, 3], [2, 3, 1]) 24 | expect(schema).to_not be_same([1, 2, 3], [1, 2]) 25 | expect(schema).to_not be_same([1, 2], [1, 2, 3]) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/hako/schema/unordered_array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/schema/integer' 5 | require 'hako/schema/unordered_array' 6 | 7 | RSpec.describe Hako::Schema::UnorderedArray do 8 | let(:schema) { described_class.new(subschema) } 9 | let(:subschema) { Hako::Schema::Integer.new } 10 | 11 | describe '#valid?' do 12 | it do 13 | expect(schema).to be_valid([1, 2, 3]) 14 | expect(schema).to_not be_valid(nil) 15 | expect(schema).to_not be_valid([1, nil]) 16 | expect(schema).to_not be_valid([1, '2']) 17 | end 18 | end 19 | 20 | describe '#same?' do 21 | it do 22 | expect(schema).to be_same([1, 2, 3], [1, 2, 3]) 23 | expect(schema).to be_same([1, 2, 3], [2, 3, 1]) 24 | expect(schema).to_not be_same([1, 2, 3], [1, 2]) 25 | expect(schema).to_not be_same([1, 2], [1, 2, 3]) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/ecs-elbv2.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | role: 'ECSServiceRole', 8 | elb_v2: { 9 | vpc_id: 'vpc-11111111', 10 | health_check_path: '/site/sha', 11 | listeners: [ 12 | { 13 | port: 80, 14 | protocol: 'HTTP', 15 | }, 16 | { 17 | port: 443, 18 | protocol: 'HTTPS', 19 | ssl_policy: 'ELBSecurityPolicy-2016-08', 20 | certificate_arn: 'arn:aws:acm:ap-northeast-1:012345678901:certificate/01234567-89ab-cdef-0123-456789abcdef', 21 | }, 22 | ], 23 | subnets: [ 24 | 'subnet-11111111', 25 | 'subnet-22222222', 26 | ], 27 | security_groups: [ 28 | 'sg-11111111', 29 | ], 30 | }, 31 | }, 32 | app: { 33 | image: 'busybox', 34 | cpu: 32, 35 | memory: 64, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /spec/hako/schema/table_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/schema/integer' 5 | require 'hako/schema/string' 6 | require 'hako/schema/table' 7 | 8 | RSpec.describe Hako::Schema::Table do 9 | let(:schema) { described_class.new(key_schema, val_schema) } 10 | let(:key_schema) { Hako::Schema::String.new } 11 | let(:val_schema) { Hako::Schema::Integer.new } 12 | 13 | describe '#valid?' do 14 | it do 15 | expect(schema).to be_valid('foo' => 1, 'bar' => 2) 16 | expect(schema).to_not be_valid('foo' => 1, 'bar' => '2') 17 | expect(schema).to_not be_valid('foo' => 1, bar: 2) 18 | expect(schema).to be_valid({}) 19 | end 20 | end 21 | 22 | describe '#same?' do 23 | it do 24 | expect(schema).to be_same({ 'foo' => 1, 'bar' => 2 }, { 'foo' => 1, 'bar' => 2 }) 25 | expect(schema).to_not be_same({ 'foo' => 1, 'bar' => 2 }, { 'foo' => 1, 'bar' => 2, 'baz' => 3 }) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/secretsmanager.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 1, 7 | role: 'ECSServiceRole', 8 | }, 9 | app: { 10 | image: 'busybox', 11 | cpu: 32, 12 | memory: 64, 13 | secrets: [ 14 | { 15 | name: 'SECRET_VALUE', 16 | value_from: 'arn:aws:secretsmanager:ap-northeast-1:012345678901:secret:hoge/fuga1-abcdef:::', 17 | }, 18 | { 19 | name: 'SECRET_MESSAGE1', 20 | value_from: 'arn:aws:secretsmanager:ap-northeast-1:012345678901:secret:hoge/fuga2-abcdef:SECRET_MESSAGE1::', 21 | }, 22 | { 23 | name: 'SECRET_MESSAGE2', 24 | value_from: 'arn:aws:secretsmanager:ap-northeast-1:012345678901:secret:hoge/fuga2-abcdef:SECRET_MESSAGE2::', 25 | }, 26 | { 27 | name: 'SECRET_MESSAGE3', 28 | value_from: 'arn:aws:secretsmanager:us-east-1:012345678901:secret:hoge/fuga3-abcdef:SECRET_MESSAGE3::', 29 | }, 30 | ], 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /lib/hako/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hako/error' 4 | require 'hako/jsonnet_loader' 5 | require 'hako/yaml_loader' 6 | require 'pathname' 7 | 8 | module Hako 9 | class Application 10 | # @!attribute [r] id 11 | # @return [String] 12 | # @!attribute [r] root_path 13 | # @return [Pathname] 14 | # @!attribute [r] definition 15 | # @return [Hash] 16 | attr_reader :id, :root_path, :definition 17 | 18 | def initialize(definition_path, expand_variables: true, ask_keys: false) 19 | path = Pathname.new(definition_path) 20 | @id = path.basename.sub_ext('').to_s 21 | @root_path = path.parent 22 | @definition = 23 | case path.extname 24 | when '.yml', '.yaml' 25 | YamlLoader.new.load(path) 26 | when '.jsonnet', '.json' 27 | JsonnetLoader.new(self, expand_variables: expand_variables, ask_keys: ask_keys).load(path) 28 | else 29 | raise Error.new("Unknown extension: #{path}") 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/hako/schema/nullable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/schema/nullable' 5 | require 'hako/schema/integer' 6 | 7 | RSpec.describe Hako::Schema::Nullable do 8 | let(:schema) { described_class.new(subschema) } 9 | let(:subschema) { Hako::Schema::Integer.new } 10 | 11 | describe '#valid?' do 12 | it do 13 | expect(schema).to be_valid(100) 14 | expect(schema).to be_valid(nil) 15 | expect(schema).to_not be_valid('100') 16 | end 17 | end 18 | 19 | describe '#same?' do 20 | context 'when both sides are non-null' do 21 | it do 22 | expect(schema).to be_same(100, 100) 23 | expect(schema).to_not be_same(100, 200) 24 | end 25 | end 26 | 27 | context 'when one side is null' do 28 | it do 29 | expect(schema).to_not be_same(nil, 100) 30 | expect(schema).to_not be_same(100, nil) 31 | end 32 | end 33 | 34 | context 'when both sides are null' do 35 | it do 36 | expect(schema).to be_same(nil, nil) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | require 'simplecov' 5 | 6 | SimpleCov.formatters = [ 7 | SimpleCov::Formatter::HTMLFormatter, 8 | ] 9 | SimpleCov.start do 10 | add_filter __dir__ 11 | end 12 | 13 | module SpecHelper 14 | def fixture_root 15 | Pathname.new(__FILE__).dirname.join('fixtures') 16 | end 17 | end 18 | 19 | RSpec.configure do |config| 20 | config.expect_with :rspec do |expectations| 21 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 22 | end 23 | 24 | config.mock_with :rspec do |mocks| 25 | mocks.verify_partial_doubles = true 26 | end 27 | 28 | config.filter_run :focus 29 | config.run_all_when_everything_filtered = true 30 | 31 | config.example_status_persistence_file_path = 'spec/examples.txt' 32 | 33 | config.disable_monkey_patching! 34 | 35 | config.warnings = true 36 | 37 | if config.files_to_run.one? 38 | config.default_formatter = 'doc' 39 | end 40 | 41 | config.profile_examples = 10 42 | 43 | config.order = :random 44 | Kernel.srand config.seed 45 | 46 | config.include(SpecHelper) 47 | end 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Kohei Suzuki 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/hako/schema/structure_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/schema/integer' 5 | require 'hako/schema/structure' 6 | 7 | RSpec.describe Hako::Schema::Structure do 8 | let(:schema) { described_class.new } 9 | 10 | before do 11 | schema.member(:foo, Hako::Schema::Integer.new) 12 | schema.member(:bar, Hako::Schema::Integer.new) 13 | end 14 | 15 | describe '#valid?' do 16 | it do 17 | expect(schema).to be_valid(foo: 1, bar: 2) 18 | expect(schema).to_not be_valid(foo: 1) 19 | expect(schema).to_not be_valid([bar: 2]) 20 | expect(schema).to_not be_valid(foo: '1', bar: 2) 21 | expect(schema).to be_valid(foo: 1, bar: 2, baz: '3') 22 | end 23 | end 24 | 25 | describe '#same?' do 26 | it do 27 | expect(schema).to be_same({ foo: 1, bar: 2 }, { foo: 1, bar: 2 }) 28 | expect(schema).to_not be_same({ foo: 1, bar: 2 }, { foo: 1, bar: 3 }) 29 | expect(schema).to be_same({ foo: 1, bar: 2, baz: 3 }, { foo: 1, bar: 2, baz: 300 }) 30 | expect(schema).to be_same({ foo: 1, bar: 2, baz: 300 }, { foo: 1, bar: 2 }) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/hello-fargate-batch.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'us-east-1', 5 | cluster: 'eagletmt', 6 | // Fargate 7 | execution_role_arn: 'arn:aws:iam::012345678901:role/ecsTaskExecutionRole', 8 | cpu: '1024', 9 | memory: '2048', 10 | ephemeral_storage: { 11 | size_in_gi_b: '25', 12 | }, 13 | requires_compatibilities: ['FARGATE'], 14 | network_mode: 'awsvpc', 15 | launch_type: 'FARGATE', 16 | network_configuration: { 17 | awsvpc_configuration: { 18 | subnets: ['subnet-XXXXXXXX'], 19 | security_groups: [], 20 | assign_public_ip: 'DISABLED', 21 | }, 22 | }, 23 | }, 24 | app: { 25 | image: 'ryotarai/hello-sinatra', 26 | cpu: 1024, 27 | memory: 256, 28 | memory_reservation: 128, 29 | log_configuration: { 30 | log_driver: 'awslogs', 31 | options: { 32 | 'awslogs-group': '/ecs/hello-fargate-batch', 33 | 'awslogs-region': 'us-east-1', 34 | 'awslogs-stream-prefix': 'ecs', 35 | }, 36 | }, 37 | }, 38 | scripts: [ 39 | (import './create_aws_cloud_watch_logs_log_group.libsonnet'), 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /examples/hello-lb.jsonnet: -------------------------------------------------------------------------------- 1 | local fileProvider = std.native('provide.file'); 2 | local provide(name) = fileProvider(std.toString({ path: 'hello.env' }), name); 3 | 4 | { 5 | scheduler: { 6 | type: 'ecs', 7 | region: 'ap-northeast-1', 8 | cluster: 'eagletmt', 9 | desired_count: 2, 10 | role: 'ecsServiceRole', 11 | // dynamic_port_mapping cannot be enabled with elb 12 | elb: { 13 | listeners: [ 14 | { 15 | load_balancer_port: 80, 16 | protocol: 'HTTP', 17 | }, 18 | ], 19 | subnets: ['subnet-XXXXXXXX', 'subnet-YYYYYYYY'], 20 | security_groups: ['sg-ZZZZZZZZ'], 21 | }, 22 | }, 23 | app: { 24 | image: 'ryotarai/hello-sinatra', 25 | memory: 128, 26 | cpu: 256, 27 | env: { 28 | PORT: '3000', 29 | MESSAGE: std.format('%s-san', provide('username')), 30 | }, 31 | }, 32 | sidecars: { 33 | front: { 34 | image_tag: 'hako-nginx', 35 | memory: 32, 36 | cpu: 32, 37 | }, 38 | }, 39 | scripts: [ 40 | (import 'front.libsonnet') + { 41 | backend_port: 3000, 42 | locations: { 43 | '/': { 44 | allow_only_from: ['10.0.0.0/24'], 45 | }, 46 | }, 47 | }, 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | DisplayCopNames: true 5 | TargetRubyVersion: 3.1 6 | NewCops: disable 7 | 8 | Layout/FirstArgumentIndentation: 9 | Enabled: false 10 | Layout/LineLength: 11 | Enabled: false 12 | 13 | Naming/PredicateName: 14 | Enabled: false 15 | Naming/MethodParameterName: 16 | Enabled: false 17 | Naming/MemoizedInstanceVariableName: 18 | Enabled: false 19 | 20 | Style/Alias: 21 | EnforcedStyle: prefer_alias_method 22 | Style/GuardClause: 23 | Enabled: false 24 | Style/HashSyntax: 25 | Exclude: 26 | - 'Rakefile' 27 | Style/IfUnlessModifier: 28 | Enabled: false 29 | Style/Next: 30 | Enabled: false 31 | Style/NumericLiterals: 32 | Enabled: false 33 | Style/PercentLiteralDelimiters: 34 | PreferredDelimiters: 35 | '%w': '[]' 36 | '%i': '[]' 37 | Style/RaiseArgs: 38 | EnforcedStyle: compact 39 | Style/RedundantCondition: 40 | Enabled: false 41 | Style/SignalException: 42 | Enabled: false 43 | Style/SoleNestedConditional: 44 | Enabled: false 45 | Style/StderrPuts: 46 | Enabled: false 47 | Style/TrailingCommaInArguments: 48 | Enabled: false 49 | Style/TrailingCommaInArrayLiteral: 50 | Enabled: false 51 | Style/TrailingCommaInHashLiteral: 52 | Enabled: false 53 | -------------------------------------------------------------------------------- /examples/hello-internal-nlb.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | scheduler: { 3 | type: 'ecs', 4 | region: 'ap-northeast-1', 5 | cluster: 'eagletmt', 6 | desired_count: 2, 7 | role: 'ecsServiceRole', 8 | // health_check_grace_period_seconds: 0, 9 | elb_v2: { 10 | // NLB can not have 11 | // * health check path 12 | // * securit group 13 | scheme: 'internal', 14 | type: 'network', 15 | // VPC id where the target group is located 16 | vpc_id: 'vpc-WWWWWWWW', 17 | listeners: [ 18 | { 19 | port: 80, 20 | protocol: 'TCP', 21 | }, 22 | ], 23 | subnets: [ 24 | 'subnet-XXXXXXXX', 25 | 'subnet-YYYYYYYY', 26 | ], 27 | container_name: 'app', 28 | container_port: 80, 29 | // If you want enalbed cross zone load balancing, then specify attribute. 30 | // load_balancer_attributes: { 31 | // 'load_balancing.cross_zone.enabled': 'true', 32 | // }, 33 | }, 34 | }, 35 | app: { 36 | image: 'nginx', 37 | memory: 128, 38 | cpu: 256, 39 | port_mappings: [ 40 | { 41 | container_port: 80, 42 | host_port: 0, 43 | protocol: 'TCP', 44 | }, 45 | ], 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /spec/fixtures/jsonnet/default_with_depends_on.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | app: { 3 | image: 'app-image', 4 | links: ['redis'], 5 | depends_on: [ 6 | { container_name: 'init2', condition: 'SUCCESS' }, 7 | ], 8 | mount_points: [ 9 | { source_volume: 'data', container_path: '/data', read_only: true }, 10 | ], 11 | }, 12 | sidecars: { 13 | redis: { 14 | image_tag: 'redis', 15 | links: ['memcached'], 16 | }, 17 | memcached: { 18 | image_tag: 'memcached', 19 | }, 20 | fluentd: { 21 | image_tag: 'fluentd', 22 | links: ['redis'], 23 | }, 24 | init: { 25 | essential: false, 26 | image_tag: 'busybox', 27 | command: ['mkdir', '-p', '/data/mydir'], 28 | mount_points: [ 29 | { source_volume: 'data', container_path: '/data' }, 30 | ], 31 | }, 32 | init2: { 33 | essential: false, 34 | image_tag: 'busybox', 35 | command: ['touch', '/data/mydir/ok.txt'], 36 | depends_on: [ 37 | { container_name: 'init', condition: 'SUCCESS' }, 38 | ], 39 | mount_points: [ 40 | { source_volume: 'data', container_path: '/data' }, 41 | ], 42 | }, 43 | }, 44 | volumes: { 45 | data: {}, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /spec/hako/schema/with_default_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'hako/schema/integer' 5 | require 'hako/schema/with_default' 6 | 7 | RSpec.describe Hako::Schema::WithDefault do 8 | let(:schema) { described_class.new(subschema, default_value) } 9 | let(:default_value) { 100 } 10 | let(:subschema) { Hako::Schema::Integer.new } 11 | 12 | describe '#valid?' do 13 | it do 14 | expect(schema).to be_valid(nil) 15 | expect(schema).to be_valid(50) 16 | expect(schema).to_not be_valid('50') 17 | end 18 | end 19 | 20 | describe '#same?' do 21 | context 'when both sides satisfy subschema' do 22 | it do 23 | expect(schema).to be_same(50, 50) 24 | expect(schema).to_not be_same(70, 50) 25 | end 26 | end 27 | 28 | context 'when one side is nil' do 29 | it do 30 | expect(schema).to be_same(nil, default_value) 31 | expect(schema).to be_same(default_value, nil) 32 | expect(schema).to_not be_same(nil, 123) 33 | expect(schema).to_not be_same(123, nil) 34 | end 35 | end 36 | 37 | context 'when side sides are nil' do 38 | it do 39 | expect(schema).to be_same(nil, nil) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /examples/hello-service-discovery.jsonnet: -------------------------------------------------------------------------------- 1 | local fileProvider = std.native('provide.file'); 2 | local provide(name) = fileProvider(std.toString({ path: 'hello.env' }), name); 3 | 4 | { 5 | scheduler: { 6 | type: 'ecs', 7 | region: 'ap-northeast-1', 8 | cluster: 'eagletmt', 9 | desired_count: 2, 10 | role: 'ecsServiceRole', 11 | service_discovery: [ 12 | { 13 | container_name: 'app', 14 | container_port: 80, 15 | service: { 16 | name: 'hello-service-discovery', 17 | namespace_id: 'ns-XXXXXXXXXXXXXXXX', 18 | dns_config: { 19 | dns_records: [ 20 | { 21 | type: 'SRV', 22 | ttl: 60, 23 | }, 24 | ], 25 | }, 26 | health_check_custom_config: { 27 | failure_threshold: 1, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | app: { 34 | image: 'ryotarai/hello-sinatra', 35 | memory: 128, 36 | cpu: 256, 37 | env: { 38 | PORT: '3000', 39 | MESSAGE: std.format('%s-san', provide('username')), 40 | }, 41 | port_mappings: [ 42 | { 43 | container_port: 3000, 44 | host_port: 0, 45 | protocol: 'tcp', 46 | }, 47 | ], 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /examples/put-ecs-container-status-to-s3/index.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const BUCKET = 'ecs-task-notifications'; 4 | 5 | exports.handler = (event, context, callback) => { 6 | const s3 = new AWS.S3(); 7 | // console.log(JSON.stringify(event, null, 2)); 8 | const taskArn = event.detail.taskArn; 9 | if (event.detail.lastStatus === 'RUNNING' && event.detail.containers.every((container) => container.lastStatus === 'RUNNING')) { 10 | console.log(`${taskArn} started`); 11 | s3.putObject({ 12 | Bucket: BUCKET, 13 | Key: `task_statuses/${taskArn}/started.json`, 14 | Body: JSON.stringify(event), 15 | }, (err, data) => { 16 | if (err) { 17 | console.log(err); 18 | callback(err); 19 | } else { 20 | callback(null, 'Started'); 21 | } 22 | }); 23 | } else if (event.detail.lastStatus === 'STOPPED') { 24 | console.log(`${taskArn} stopped`); 25 | s3.putObject({ 26 | Bucket: BUCKET, 27 | Key: `task_statuses/${taskArn}/stopped.json`, 28 | Body: JSON.stringify(event), 29 | }, (err, data) => { 30 | if (err) { 31 | console.log(err); 32 | callback(err); 33 | } else { 34 | callback(null, 'Stopped'); 35 | } 36 | }); 37 | } else { 38 | callback(null, 'Skipped'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /examples/hello.jsonnet: -------------------------------------------------------------------------------- 1 | local fileProvider = std.native('provide.file'); 2 | local provide(name) = fileProvider(std.toString({ path: 'hello.env' }), name); 3 | 4 | { 5 | scheduler: { 6 | type: 'ecs', 7 | region: 'ap-northeast-1', 8 | cluster: 'eagletmt', 9 | desired_count: 2, 10 | task_role_arn: 'arn:aws:iam::012345678901:role/HelloRole', 11 | deployment_configuration: { 12 | maximum_percent: 200, 13 | minimum_healthy_percent: 50, 14 | }, 15 | }, 16 | app: { 17 | image: 'ryotarai/hello-sinatra', 18 | memory: 128, 19 | cpu: 256, 20 | health_check: { 21 | command: [ 22 | 'CMD-SHELL', 23 | 'curl -f http://localhost:3000/ || exit 1', 24 | ], 25 | interval: 30, 26 | timeout: 5, 27 | retries: 3, 28 | start_period: 1, 29 | }, 30 | links: [ 31 | 'redis:redis', 32 | ], 33 | env: { 34 | PORT: '3000', 35 | MESSAGE: std.format('%s-san', provide('username')), 36 | }, 37 | }, 38 | sidecars: { 39 | front: { 40 | image_tag: 'hako-nginx', 41 | memory: 32, 42 | cpu: 32, 43 | }, 44 | redis: { 45 | image_tag: 'redis:3.0', 46 | cpu: 64, 47 | memory: 512, 48 | }, 49 | }, 50 | scripts: [ 51 | (import 'front.libsonnet') + { 52 | backend_port: 3000, 53 | }, 54 | ], 55 | } 56 | -------------------------------------------------------------------------------- /lib/hako/scheduler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hako 4 | class Scheduler 5 | class ValidationError < Error 6 | end 7 | 8 | # @param [String] app_id 9 | # @param [Hash] options 10 | # @param [Hash] volumes 11 | # @param [Array