App-FargateStack

 view release on metacpan or  search on metacpan

lib/App/FargateStack/Checker.pm  view on Meta::CPAN

package App::FargateStack::Checker;

use strict;
use warnings;

use App::FargateStack::Builder::Utils qw(choose);
use Carp;
use Carp::Always;
use CLI::Simple::Constants qw(:booleans %LOG_LEVELS);
use Data::Dumper;
use English qw(-no_match_vars);
use Getopt::Long qw(GetOptions);
use List::Util qw(any none uniq);
use Text::ASCIITable::EasyTable;
use Sub::Util qw(subname);

use parent qw(CLI::Simple);

__PACKAGE__->use_log4perl( log_level => 'info' );

caller or __PACKAGE__->main();

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

  my @rows;

  push @rows, check( $self, \&check_credentials );
  push @rows, check( $self, \&check_service_linked_roles );
  push @rows, check( $self, \&check_vpc_and_subnets );
  push @rows, check( $self, \&check_egress );
  push @rows, check( $self, \&check_ecs_permissions );
  push @rows, check( $self, \&check_elbv2_permissions );
  push @rows, check( $self, \&check_logs_permissions );
  push @rows, check( $self, \&check_ecr_access );
  push @rows, check( $self, \&check_events_permissions );
  push @rows, check( $self, \&check_passrole );

  if ( $self->get_dns )     { push @rows, check_route53($self); }
  if ( $self->get_https )   { push @rows, check_acm($self); }
  if ( $self->get_secrets ) { push @rows, check_secrets_hint($self); }

  my $exit = render_table( $self, \@rows );

  my ( $caps, $m ) = summarize_capabilities( \@rows );
  # Temporary debug to stderr
  my %sets = (
    'HTTP services' =>
      [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'ELBv2 perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
    'HTTPS service' =>
      [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'ELBv2 perms', 'CloudWatch Logs perms', 'iam:PassRole', 'ACM' ],
    'Scheduled tasks' =>
      [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'Events perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
    'One-shot tasks'  => [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
    'Daemon services' => [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
  );

  foreach my $cap ( sort keys %sets ) {
    my @missing = grep { !exists $m->{$_} } @{ $sets{$cap} };
    if (@missing) {
      print {*STDERR} sprintf "capability[%s] missing checks: %s\n", $cap, join q{, }, @missing;
    }
  }

  render_capabilities( $caps, $self->get_account, $self->get_region );

  return $exit;
}

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

  my ( undef, $name ) = split /_/xsm, subname($sub);

  $self->get_logger->info( sprintf 'checking %s...', $name );

  return $sub->($self);
}

my %clients;

########################################################################
sub new_client {
########################################################################
  my ( $class, @args ) = @_;

lib/App/FargateStack/Checker.pm  view on Meta::CPAN

    $err = $EVAL_ERROR || 'unknown error';
  };

  return ( !!$out, $out, $err );
}

########################################################################
sub render_table {
########################################################################
  my ( $self, $rows ) = @_;

  my $title = sprintf 'Fargate environment preflight (profile: [%s] region: [%s], Route53 profile: [%s])',
    $self->get_profile, $self->get_region, $self->get_route53_profile;

  my $exit_code = 0;

  foreach my $r ( @{$rows} ) {
    if ( $r->{Status} eq 'FAIL' ) {
      $exit_code = 2;
    }
    if ( $r->{Status} eq 'WARN' && $exit_code == 0 ) {
      $exit_code = 1;
    }
  }

  print {*STDOUT} easy_table(
    table_options => { headingText => $title },
    columns       => [qw(Check Status Detail)],
    data          => $rows,
  );

  return $exit_code;
}

########################################################################
sub row_ok   { return { Check => $_[0], Status => 'PASS', Detail => $_[1] // q{} }; }
sub row_warn { return { Check => $_[0], Status => 'WARN', Detail => $_[1] // q{} }; }
sub row_fail { return { Check => $_[0], Status => 'FAIL', Detail => $_[1] // q{} }; }
########################################################################

########################################################################
sub check_events_permissions {
########################################################################
  my ($opt) = @_;

  my $events = new_client( 'App::Events', $opt );

  # Read-only probe; no mutation. Either of these is fine:
  # list-event-buses  => broad, minimal perms
  # list-rules        => stricter, proves rule visibility
  my ( $ok, $buses, $err ) = try_aws( sub { $events->command( 'list-event-buses' => [ '--query' => 'EventBuses[].Name' ] ) } );

  if ( !$ok ) {
    return row_fail( 'Events perms', 'Cannot list event buses' );
  }

  return row_ok( 'Events perms', 'Describe OK' );
}

########################################################################
sub check_credentials {
########################################################################
  my ($opt) = @_;

  my $sts = new_client( 'App::STS', $opt );

  my ( $ok, $out, $err ) = try_aws( sub { $sts->get_caller_identity() } );

  if ( !$ok || !$out ) {
    return row_fail( 'Credentials', 'Cannot call STS GetCallerIdentity' );
  }

  my $acct = $out->{Account} || q{};
  my $arn  = $out->{Arn}     || q{};

  $opt->set_account($acct);

  if ( !$acct ) {
    return row_fail( 'Credentials', 'Missing account in STS response' );
  }

  return row_ok( 'Credentials', "Account $acct, Principal $arn" );
}

########################################################################
sub check_service_linked_roles {
########################################################################
  my ($opt) = @_;

  my $iam = new_client( 'App::IAM', $opt );

  my @required = ('AWSServiceRoleForECS');                   # core ECS control plane
  my @advisory = ('AWSServiceRoleForElasticLoadBalancing');  # only if you use ALB/NLB

  # If you intend to use service autoscaling, include this:
  if ( $opt->{check_scaling} ) {
    push @advisory, 'AWSServiceRoleForApplicationAutoScaling_ECSService';
  }

  my @missing_req;
  my @missing_adv;

  foreach my $r (@required) {
    my ($ok) = try_aws( sub { $iam->command( 'get-role' => [ '--role-name' => $r ] ) } );
    if ( !$ok ) {
      push @missing_req, $r;
    }
  }

  foreach my $r (@advisory) {
    my ($ok) = try_aws( sub { $iam->command( 'get-role' => [ '--role-name' => $r ] ) } );
    if ( !$ok ) {
      push @missing_adv, $r;
    }
  }

  if ( @missing_req && @missing_adv ) {
    return row_warn( 'Service-linked roles',
      'Missing required: ' . join( ', ', @missing_req ) . ' ; advisory missing: ' . join( ', ', @missing_adv ) );
  }

lib/App/FargateStack/Checker.pm  view on Meta::CPAN

  my $cli = App::FargateStack::Checker->new(
    commands        => \%commands,
    default_options => \%default_options,
    option_specs    => \@option_specs,
    extra_options   => [qw(account global_options role_names route53_profile)],
  );

  my @expected_roles = (
    # Execution role and task role your builder will create
    'FargateStack/my-svc/TaskExecutionRole',
    'FargateStack/my-svc/TaskRole',
    'FargateStack/my-svc/EventsInvokeRole',
  );

  $cli->set_role_names( \@expected_roles );

  $cli->set_global_options(
    { profile => $cli->get_profile,
      region  => $cli->get_region
    }
  );

  return $cli->run();
}

1;

__END__

=pod

=head1 NAME

app-FargateStack-env.pl - Preflight checker for ECS Fargate environments

=head1 USAGE

  app-FargateStack-env.pl [options]

=head1 DESCRIPTION

Runs read-only checks against the target AWS account and region to verify
that common ECS Fargate deployment scenarios are feasible. Produces an
ASCII table with PASS/WARN/FAIL rows and a capabilities summary for:

  - HTTP services
  - HTTPS service
  - Scheduled tasks
  - One-shot tasks
  - Daemon services

No resources are created or modified. Intended as a fast “can I deploy here?”
probe for humans and CI.

=head2 Options

=over 4

=item B<--profile> I<STR>

AWS config/credentials profile to use. Defaults to C<$ENV{AWS_PROFILE}> or the
SDK’s default behavior if unset.

=item B<--region> I<STR>

AWS region to target (e.g. C<us-east-1>). Defaults to C<$ENV{AWS_REGION}> if set.

=item B<--dns> | B<--no-dns>

Enable or disable Route 53 checks. Default: B<enabled>.
Use B<--no-dns> (or B<--nodns>) to skip DNS checks.

=item B<--dns-profile> I<STR>

Alternate AWS profile for Route 53 lookups. Useful when DNS is managed in a
separate account. Falls back to C<--profile> if not provided.

=item B<--https> | B<--no-https>

Enable or disable ACM certificate checks (same region as the load balancer).
Default: B<disabled>. Turn on if you plan to deploy HTTPS.

=item B<--secrets> | B<--no-secrets>

Enable or disable Secrets Manager reachability checks. Default: B<disabled>.
When enabled, the checker verifies control-plane reachability (e.g., VPC
endpoint present or NAT available). It does not validate individual
C<GetSecretValue> permissions for task roles.

=back

=head1 OUTPUT

The main table includes rows like:

  Credentials
  Service-linked roles
  VPC/Subnets
  Egress
  ECS perms
  ELBv2 perms
  CloudWatch Logs perms
  ECR
  Events perms
  iam:PassRole
  Route 53
  ACM
  Secrets Manager

Each row has a Status of B<PASS>, B<WARN>, or B<FAIL> and a Detail string.
Typical examples:

  - Egress: PASS with NAT present; missing VPC endpoints are called out as optional.
  - Events perms: PASS when EventBridge APIs are readable (e.g., list-event-buses).
  - iam:PassRole: PASS when simulation allows passing target roles to
    C<ecs-tasks.amazonaws.com> and C<events.amazonaws.com>.

=head2 CAPABILITIES SUMMARY

After the table, a summary lists readiness for common Fargate scenarios:



( run in 0.834 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )