lib/hako/schedulers/ecs.rb in hako-2.2.0 vs lib/hako/schedulers/ecs.rb in hako-2.3.0

- old
+ new

@@ -11,10 +11,11 @@ require 'hako/schedulers/ecs_autoscaling' require 'hako/schedulers/ecs_definition_comparator' require 'hako/schedulers/ecs_elb' require 'hako/schedulers/ecs_elb_v2' require 'hako/schedulers/ecs_service_comparator' +require 'hako/schedulers/ecs_volume_comparator' module Hako module Schedulers class Ecs < Scheduler class NoTasksStarted < Error @@ -43,11 +44,11 @@ @ecs_elb_v2_options['target_type'] = 'ip' end @dynamic_port_mapping = options.fetch('dynamic_port_mapping', @ecs_elb_options.nil?) @health_check_grace_period_seconds = options.fetch('health_check_grace_period_seconds', nil) if options.key?('autoscaling') - @autoscaling = EcsAutoscaling.new(options.fetch('autoscaling'), @region, dry_run: @dry_run) + @autoscaling = EcsAutoscaling.new(options.fetch('autoscaling'), @region, ecs_elb_client, dry_run: @dry_run) end @autoscaling_group_for_oneshot = options.fetch('autoscaling_group_for_oneshot', nil) @autoscaling_topic_for_oneshot = options.fetch('autoscaling_topic_for_oneshot', nil) if @autoscaling_topic_for_oneshot && !@autoscaling_group_for_oneshot validation_error!('autoscaling_group_for_oneshot must be set when autoscaling_topic_for_oneshot is set') @@ -92,10 +93,13 @@ front_port = determine_front_port @scripts.each { |script| script.deploy_started(containers, front_port) } definitions = create_definitions(containers) if @dry_run + volumes_definition.each do |d| + print_volume_definition_in_cli_format(d) + end definitions.each do |d| print_definition_in_cli_format(d) end if @autoscaling @autoscaling.apply(Aws::ECS::Types::Service.new(cluster_arn: @cluster, service_name: @app_id)) @@ -174,10 +178,13 @@ definitions.each do |definition| definition.delete(:essential) end if @dry_run + volumes_definition.each do |d| + print_volume_definition_in_cli_format(d) + end definitions.each do |d| if d[:name] == 'app' d[:command] = commands end print_definition_in_cli_format(d, additional_env: env) @@ -403,21 +410,28 @@ end unless actual_definition # Initial deployment return true end + actual_volume_definitions = {} + actual_definition.volumes.each do |v| + actual_volume_definitions[v.name] = v + end container_definitions = {} actual_definition.container_definitions.each do |c| container_definitions[c.name] = c end if actual_definition.task_role_arn != @task_role_arn return true end - if different_volumes?(actual_definition.volumes) + if volumes_definition.any? { |definition| different_volume?(definition, actual_volume_definitions.delete(definition[:name])) } return true end + unless actual_volume_definitions.empty? + return true + end if desired_definitions.any? { |definition| different_definition?(definition, container_definitions.delete(definition[:name])) } return true end unless container_definitions.empty? return true @@ -439,27 +453,15 @@ end false end - # @param [Hash<String, Hash<String, String>>] actual_volumes + # @param [Hash] expected_volume + # @param [Aws::ECS::Types::Volume] actual_volume # @return [Boolean] - def different_volumes?(actual_volumes) - if @volumes.size != actual_volumes.size - return true - end - actual_volumes.each do |actual_volume| - expected_volume = @volumes[actual_volume.name] - if expected_volume.nil? - return true - end - if expected_volume['source_path'] != actual_volume.host.source_path - return true - end - end - - false + def different_volume?(expected_volume, actual_volume) + EcsVolumeComparator.new(expected_volume).different?(actual_volume) end # @param [Hash] expected_container # @param [Aws::ECS::Types::ContainerDefinition] actual_container # @return [Boolean] @@ -532,17 +534,32 @@ end end raise Error.new('Unable to register task definition for oneshot due to too many client errors') end - # @return [Hash] + # @return [Array<Hash>] def volumes_definition - @volumes.map do |name, volume| - { - name: name, - host: { source_path: volume['source_path'] }, - } + @volumes_definition ||= @volumes.map do |name, volume| + definition = { name: name } + if volume.key?('docker_volume_configuration') + configuration = volume['docker_volume_configuration'] + definition[:docker_volume_configuration] = { + autoprovision: configuration['autoprovision'], + driver: configuration['driver'], + # ECS API doesn't allow 'driver_opts' to be an empty hash. + driver_opts: configuration['driver_opts'], + # ECS API doesn't allow 'labels' to be an empty hash. + labels: configuration['labels'], + scope: configuration['scope'], + } + else + # When neither 'host' nor 'docker_volume_configuration' is + # specified, ECS API treats it as if 'host' is specified without + # 'source_path'. + definition[:host] = { source_path: volume['source_path'] } + end + definition end end def describe_task_definition(family) ecs_client.describe_task_definition(task_definition: family).task_definition @@ -572,10 +589,11 @@ privileged: container.privileged, linux_parameters: container.linux_parameters, volumes_from: container.volumes_from, user: container.user, log_configuration: container.log_configuration, + health_check: container.health_check, ulimits: container.ulimits, extra_hosts: container.extra_hosts, } end @@ -1042,10 +1060,35 @@ required_cpu <= cpu && required_memory <= memory end end # @param [Hash] definition + # @return [nil] + def print_volume_definition_in_cli_format(definition) + return if definition.dig(:docker_volume_configuration, :autoprovision) + # From version 1.20.0 of ECS agent, a local volume is provisioned when + # 'host' is specified without 'source_path'. + return if definition.dig(:host, :source_path) + + cmd = %w[docker volume create] + if (configuration = definition[:docker_volume_configuration]) + if configuration[:driver] + cmd << '--driver' << configuration[:driver] + end + (configuration[:driver_opts] || {}).each do |k, v| + cmd << '--opt' << "#{k}=#{v}" + end + (configuration[:labels] || {}).each do |k, v| + cmd << '--label' << "#{k}=#{v}" + end + end + cmd << definition[:name] + puts cmd.join(' ') + nil + end + + # @param [Hash] definition # @param [Hash<String, String>] additional_env # @return [nil] def print_definition_in_cli_format(definition, additional_env: {}) cmd = %w[docker run] cmd << '--name' << definition.fetch(:name) @@ -1067,17 +1110,18 @@ cmd << '--label' << "#{key}=#{val}" end end definition.fetch(:mount_points).each do |mount_point| source_volume = mount_point.fetch(:source_volume) - v = @volumes[source_volume] - if v - cmd << '--volume' << "#{v.fetch('source_path')}:#{mount_point.fetch(:container_path)}#{mount_point[:read_only] ? ':ro' : ''}" - else - raise "Could not find volume #{source_volume}" - end + v = volumes_definition.find { |d| d[:name] == source_volume } + raise "Could not find volume #{source_volume}" unless v + source = v.dig(:host, :source_path) || source_volume + cmd << '--volume' << "#{source}:#{mount_point.fetch(:container_path)}#{mount_point[:read_only] ? ':ro' : ''}" end + definition.fetch(:volumes_from).each do |volumes_from| + cmd << '--volumes-from' << "#{volumes_from.fetch(:source_container)}#{volumes_from[:read_only] ? ':ro' : ''}" + end if definition[:privileged] cmd << '--privileged' end if definition[:linux_parameters] if definition[:linux_parameters][:capabilities] @@ -1101,11 +1145,20 @@ end cmd << "--device=#{opts}" end end - if definition[:init_process_enabled] + if definition[:linux_parameters][:init_process_enabled] cmd << '--init' + end + + if definition[:linux_parameters][:shared_memory_size] + cmd << '--shm-size' << "#{definition[:linux_parameters][:shared_memory_size]}m" + end + + definition[:linux_parameters].fetch(:tmpfs, []).each do |tmpfs| + options = ["size=#{tmpfs[:size]}m"].concat(tmpfs[:mount_options]) + cmd << '--tmpfs' << "#{tmpfs[:container_path]}:#{options.join(',')}" end end definition.fetch(:volumes_from).each do |volumes_from| p volumes_from end