Group
Extension

App-FargateStack/lib/App/FargateStack/Builder/Events.pm

package App::FargateStack::Builder::Events;

use strict;
use warnings;

use App::FargateStack::Builder::Utils qw(log_die);
use App::FargateStack::Constants;
use Carp;
use Data::Compare;
use Data::Dumper;
use English qw(-no_match_vars);
use JSON;

use Role::Tiny;

########################################################################
sub build_events {
########################################################################
  my ($self) = @_;

  my ( $config, $dryrun, $cluster, $tasks ) = $self->common_args(qw(config dryrun cluster tasks));

  my @events = grep { $tasks->{$_}->{type} eq 'task' && $tasks->{$_}->{schedule} } keys %{$tasks};

  if ( !@events ) {
    $self->log_info('events: no events to build');
    return;
  }

  $self->log_info( 'events: [%d] event(s) to build', scalar @events );

  $self->log_info('events: constructing IAM role for events...');

  $self->build_events_iam_role(@events);  # task names

  foreach my $task_name (@events) {
    $self->log_info( 'events: building event for: [%s]', $task_name );

    my $task = $tasks->{$task_name};

    # ok to be undef, as App::Events will provide default
    my $event_bus_name = $task->{event_bus_name};

    my $event = $self->fetch_events;

    my $schedule = $task->{schedule};

    if ( !$schedule ) {
      $self->log_warn( 'events: no schedule for task: [%s]...skipping', $task_name );
      return;
    }

    $self->log_info( 'events: found a schedule...validating schedule: [%s]', $schedule );

    my $valid_schedule = $event->validate_schedule($schedule);

    croak sprintf "ERROR: invalid schedule expression: %s\nSee: %s", $schedule, $EVENT_SCHEDULER_TYPE_URL
      if !$valid_schedule;

    my $rule_name = sprintf '%s-schedule', $task_name;

    if ( $valid_schedule ne $schedule ) {
      $self->log_warn( sprintf 'events: [%s] schedule modified [%s] automatically for you...', $schedule, $valid_schedule );

      $task->{schedule} = $valid_schedule;
      $schedule = $valid_schedule;
    }

    $self->log_info( 'events: checking if rule [%s] exists or schedule [%s] changed', $rule_name, $schedule );

    my $exists = $event->rule_exists( $rule_name, $schedule );

    my $rule_exists = $exists ? q{} : 'does not ';

    my $schedule_changed = $exists == -1 ? q {} : 'not ';

    my $level = $rule_exists ? 'warn' : 'info';
    $level = $schedule_changed ? 'info' : 'error';
    $level = "log_$level";

    $self->$level( 'events: rule [%s] %sexists and schedule [%s] has %schanged',
      $rule_name, $rule_exists, $schedule, $schedule_changed );

    if ( $exists == -1 || $self->taskdef_has_changed($task_name) ) {
      if ( !$dryrun ) {
        $self->log_error( 'events: attempting to delete rule in case target does not exist [%s]...%s', $rule_name, $dryrun );

        my $result = $event->delete_rule($rule_name);
        $event->check_result(
          message => 'could not delete rule: [%s]',
          params  => [$rule_name],
          regexp  => qr/has\stargets/xmsi
        );
      }
    }

    my $action = !$exists ? 'created' : 'replaced';

    if ( !$exists || ( $exists == -1 || $self->taskdef_has_changed($task_name) ) ) {

      $self->inc_required_resources( 'events:rule' => [$rule_name] );
      $self->get_logger->warn( sprintf 'events: [%s] rule will be %s...%s', $rule_name, $action, $dryrun );

      $self->log_warn( sprintf 'events: [%s] rule will be %s...%s', $rule_name, $action, $dryrun );

      if ( !$dryrun ) {
        $event->put_rule( rule_name => $rule_name, schedule => $schedule, state => $TRUE );
        $event->check_result( message => 'ERROR: could not create rule [%s]', $rule_name );
      }

      $self->get_logger->warn( sprintf 'events: [%s] rule %s successfully', $rule_name, $action );
    }
    else {
      $self->get_logger->info( sprintf 'events: rule [%s] exists...skipping', $rule_name );
      $self->inc_existing_resources( 'events:rule' => $rule_name );
    }

    $self->create_event_target($task_name);
  }

  return;
}

########################################################################
sub create_event_target {
########################################################################
  my ( $self, $task_name ) = @_;

  my ( $config, $dryrun, $tasks ) = $self->common_args(qw(config dryrun tasks));

  my $region = $self->get_region;

  my $account = $self->get_account;

  my $task = $tasks->{$task_name};

  my $events_role = $config->{events_role};

  my $task_definition_arn = $task->{arn} // $EMPTY;

  my @subnets = sort @{ $self->get_subnets->{private} // $self->get_subnets->{public} };

  my $security_group = $config->{security_groups}->{fargate}->{group_id};

  my $tags = $self->build_run_tags( $task_name => $task );

  my $target = [
    { Id            => $self->create_default( 'rule-id', $task_name ),
      Arn           => $config->{cluster}->{arn},
      RoleArn       => $events_role->{arn},
      EcsParameters => {
        Tags                 => $tags,
        TaskDefinitionArn    => $task_definition_arn,
        TaskCount            => 1,
        LaunchType           => 'FARGATE',
        Group                => 'schedule:' . $task_name,
        EnableECSManagedTags => JSON::true,
        EnableExecuteCommand => JSON::false,
        PropagateTags        => 'TASK_DEFINITION',
        NetworkConfiguration => {
          awsvpcConfiguration => {
            Subnets        => [ $subnets[0] ],
            SecurityGroups => [$security_group],
            AssignPublicIp => 'DISABLED'
          }
        }
      }
    }
  ];

  my $rule_name = $self->create_default( 'rule-name', $task_name );

  my $event = $self->fetch_events;

  my $current_target = $event->target_exists($rule_name);

  my $replace_target = $current_target ? $TRUE : $FALSE;

  # compare $target vs $current_target
  if ( $current_target && !Compare( $target->[0], $current_target ) ) {

    $self->display_diffs( $current_target, $target->[0], { style => 'Table', title => 'rule target differs' } );
    $replace_target = $TRUE;
  }

  $self->log_trace(
    sub {
      return Dumper(
        [ current_target      => $current_target,
          target              => $target,
          task_definition_arn => $task_definition_arn,
          replace_target      => $replace_target,
        ]
      );
    }
  );

  my $ecs_parameters = $current_target->{EcsParameters} // {};

  my $current_task_definition_arn = $ecs_parameters->{TaskDefinitionArn} // $EMPTY;

  if ( !$replace_target && $current_task_definition_arn && $current_task_definition_arn eq $task_definition_arn ) {
    $self->log_info( sprintf 'events: [%s] target exists...skipping', $task_name );
    $self->inc_existing_resources( 'events:target' => [$task_name] );

    return;
  }
  elsif ( $current_task_definition_arn && $current_task_definition_arn eq $task_definition_arn ) {
    $replace_target = -1;

    $self->log_warn( sprintf 'events: [%s] task ARNs differ [%s] <=> [%s]',
      $task_name, $current_task_definition_arn, $task_definition_arn );
  }

  my $action = $current_target ? 'replaced' : 'created';

  $self->log_warn( sprintf 'events: target [%s] will be %s...%s', $task_name, $action, $dryrun );

  $self->inc_required_resources( 'events:target' => [$task_name] );

  return
    if $dryrun;

  if ($current_target) {
    $self->log_warn( 'events: deleting target for rule [%s]...', $rule_name );
    $event->remove_targets( $rule_name, $self->create_default( 'rule-id', $task_name ) );
  }

  $self->log_warn( 'events: creating target for rule [%s]...', $rule_name );

  my $result = $event->put_targets( $rule_name, $target );
  $event->check_result(
    message => 'ERROR: could not create target [%s] for rule [%s]',
    params  => [ to_json( $target, { pretty => $TRUE } ), $rule_name ]
  );

  return;
}

########################################################################
sub build_events_iam_role {
########################################################################
  my ($self) = @_;

  my ( $config, $dryrun ) = $self->common_args(qw(config dryrun));

  my $iam = $self->fetch_iam;

  my $role_config = $config->{events_role} // {};

  my ( $role_name, $role_arn ) = $self->create_events_role();

  my $policy_name = $self->create_default( 'policy-name', 'events' );
  @{$role_config}{qw(name arn policy_name)} = ( $role_name, $role_arn, $policy_name );

  my $policy = $iam->get_role_policy( $role_name, $policy_name );

  my @statement = $self->add_events_policy();

  my $role_policy = {
    Version   => $IAM_POLICY_VERSION,
    Statement => \@statement,
  };

  if ( $policy && Compare( $policy, $role_policy ) ) {
    $self->log_info( 'iam:role-policy: policy [%s] for [%s] exists...%s',
      $policy_name, $role_name, $self->get_cache || 'skipping' );

    $self->inc_existing_resources( 'iam:role-policy' => [$policy_name] );

    return;
  }

  if ($policy) {
    $self->display_diffs( $policy, $role_policy, { title => 'event policy has changed' } );
  }

  $self->log_warn(
    'iam:role-policy: policy [%s] will be %s for [%s]...%s',
    $policy_name, ( $policy ? 'updated' : 'created' ),
    $role_name, $dryrun
  );

  $self->inc_required_resources( 'iam:policy' => [$policy_name] );

  return
    if $dryrun;

  $iam->put_role_policy( $role_name, $policy_name, $role_policy );
  $iam->check_result(
    message => 'ERROR: could not %s policy [%s] for [%s]',
    params  => [ ( $policy ? 'update' : 'create' ), $policy_name, $role_name ]
  );

  $self->log_warn( 'iam:role-policy: policy [%s] %s successfully for [%s]...',
    $policy_name, ( $policy ? 'updated' : 'created' ), $role_name );

  return;
}

########################################################################
sub update_rule_state {
########################################################################
  my ( $self, $state ) = @_;

  my ( $config, $tasks, $dryrun ) = $self->common_args(qw(config tasks dryrun));

  my ($task_name) = $self->get_args;

  my $err;

  if ( !$task_name ) {
    ( $task_name, $err ) = grep { defined $tasks->{$_}->{schedule} } keys %{$tasks};
  }

  croak sprintf "%s %s-scheduled-task task-name\n", $ENV{SCRIPT_NAME}, $state ? 'enable' : 'disable'
    if !$task_name || $err;

  my $rule_name = sprintf '%s-schedule', $task_name;

  require App::Events;

  my $event  = App::Events->new( $self->get_global_options );
  my $result = $event->describe_rule( $rule_name, '{state: State, schedule: ScheduleExpression}' );

  log_die( $self, "could not describe rule: [%s]\n%s", $rule_name, $event->get_error )
    if !$result;

  $self->log_warn( 'events: current state: [%s] for rule [%s]...will be updated do [%s]...%s',
    $result->{state}, $rule_name, ( $state ? 'ENABLED' : 'DISABLED' ), $dryrun );

  return
    if $dryrun;

  $result = $state ? $event->enable_rule($rule_name) : $event->disable_rule($rule_name);

  log_die( $self, "could not update rule: [%s]\n%s", $rule_name, $event->get_error )
    if !$result && $event->get_error;

  return;
}

########################################################################
sub create_events_role { return shift->create_role( @_, 'events' ); }
########################################################################

########################################################################
sub add_events_policy {
########################################################################
  my ($self) = @_;

  my @events = $self->has_events;

  my $region       = $self->get_region;
  my $account      = $self->get_account;
  my $cluster_name = $self->get_config->{cluster}->{name};

  my $role_arn = $self->get_config->{role}->{arn};

  my @policy_statement = (
    { Effect    => 'Allow',
      Action    => 'ecs:RunTask',
      Resource  => [ map { sprintf $TASK_DEFINITION_ARN_TEMPLATE, $region, $account, $_ } @events ],
      Condition => {
        ArnLike => {
          'ecs:cluster' => sprintf $CLUSTER_ARN_TEMPLATE,
          $region, $account, $cluster_name,
        }
      }
    },
    { Effect   => 'Allow',
      Action   => 'iam:PassRole',
      Resource => [$role_arn],
    },
    { 'Sid'      => 'TagLaunchedTasks',
      'Effect'   => 'Allow',
      'Action'   => 'ecs:TagResource',
      'Resource' => sprintf 'arn:aws:ecs:%s:%s:task/%s/*',
      $region, $account, $cluster_name,
    }
  );

  return @policy_statement;
}

########################################################################
sub has_events {
########################################################################
  my ($self) = @_;

  my ( $config, $tasks ) = $self->common_args(qw(config tasks));

  my ($schedule) = grep { $tasks->{$_}->{schedule} } keys %{$tasks};

  return $schedule;
}

########################################################################
sub fetch_events {
########################################################################
  my ($self) = @_;

  require App::Events;

  my $events = $self->get_events;

  return $events
    if $events;

  $events = App::Events->new( $self->get_global_options );

  $self->set_events($events);

  return $events;
}

########################################################################
sub build_run_tags {
########################################################################
  my ( $self, $task_name, $task ) = @_;

  my %tags = (
    %{ $task->{tags} // {} },  # user-specified tags
    'App::FargateStack:Schedule' => $task_name,
    'App::FargateStack:Type'     => 'cron',
  );

  my @tags = map { ( { Key => $_, Value => $tags{$_} } ) } sort keys %tags;
  return \@tags;
}

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.