#!/usr/bin/env perl

# AWS Elastic Container Services Examples

use strict;
use warnings;

use 5.010001;

use Amazon::API            qw( param_n service);
use Amazon::API::Constants qw(:booleans :chars);
use Amazon::API::CloudWatchLogs;
use Amazon::API::EC2;
use Amazon::API::ECS;
use APIExample qw(normalize_options dump_json);

use Carp;
use Data::Dumper;
use English       qw( -no_match_vars );
use Getopt::Long  qw(:config no_ignore_case auto_help);
use List::Util    qw(any none zip);
use Log::Log4perl qw( :easy );
use Log::Log4perl::Level;
use JSON::PP qw( decode_json );
use Pod::Usage;
use ReadonlyX;
use Scalar::Util qw( reftype );

our $VERSION = '0.1';

Log::Log4perl->easy_init( $ENV{DEBUG} ? $DEBUG : $INFO );

# global options to send to Amazon::API classes
our %GLOBAL_OPTIONS = (
  debug              => $ENV{DEBUG},
  region             => $ENV{AWS_REGION} // 'us-east-1',
  logger             => Log::Log4perl->get_logger,
  no_passkey_warning => $TRUE,
);

Readonly::Hash our %LOG_LEVELS => (
  debug => $DEBUG,
  trace => $TRACE,
  warn  => $WARN,
  error => $ERROR,
  fatal => $FATAL,
  info  => $INFO,
);

Readonly::Array our @OPTION_SPECS => qw(
  cluster-name|c=s
  debug|d
  format=s
  image-name|i=s
  log-group|l=s
  log-level=s
  task-definition=s
  task-name|t=s
  vpc-id=s
  vpc-name|v=s
);

our %DEFAULT_OPTIONS = (
  format    => 'json',
  logger    => Log::Log4perl->get_logger,
  log_level => 'info',
);

our %COMMANDS = (
  'get-default-vpc' => sub {
    my ($options) = @_;
    print {*STDOUT} get_default_vpc($options);

    return $SUCCESS;
  },

  'get-vpc-id' => sub {
    my ($options) = @_;
    print {*STDOUT} get_vpc_id($options);

    return $SUCCESS;
  },

  'get-subnets' => sub {
    my ($options) = @_;

    print {*STDOUT}
      dump_json( get_subnets( get_args( $options, qw(vpc_id key tag) ) ) );

    return $SUCCESS;
  },

  'get-log-groups' => sub {
    my ($options) = @_;

    print {*STDOUT} get_log_groups($options);

    return $SUCCESS;
  },

  'get-vpc' => sub {
    my ($options) = @_;

    my %args = get_args( $options, qw(key tag) );

    croak 'usage: get-vpc key value'
      if !$args{key} || !$args{tag};

    print {*STDOUT} get_vpc_by_tag($options);

    return $SUCCESS;
  },

  'run-task' => \&run_task,

  'create-log-group' => \&create_log_group,

  'register-task' => \&register_task,
);

########################################################################
# Elastic Container Service
########################################################################

{
  my $ecs;

  sub ecs {
    return $ecs // service( 'ecs', \%GLOBAL_OPTIONS );
  }
}
########################################################################

########################################################################
# CloudWatch Logs
########################################################################
{
  my $logs;

  sub logs {
    return $logs // service( 'CloudWatchLogs', \%GLOBAL_OPTIONS );
  }
}
########################################################################

########################################################################
# Elastic Cloud Compute
########################################################################
{
  my $ec2;

  sub ec2 {
    my (%options) = @_;

    return $ec2 // service( 'ec2', \%GLOBAL_OPTIONS );
  }
}
########################################################################

########################################################################
sub return_output {
########################################################################
  my ( $options, $output ) = @_;

  return $options->{format}
    && $options->{format} eq 'json' ? dump_json($output) : $output;
}

########################################################################
sub get_default_vpc {
########################################################################
  my ($options) = @_;

  my $vpc_list = ec2()->DescribeVpcs;

  my ($default_vpc)
    = map { $_->{IsDefault} ? $_ : () } @{ $vpc_list->{Vpcs} };

  return return_output( $options, $default_vpc );
}

########################################################################
sub get_subnets_by_vpc {
########################################################################
  my ($options) = @_;

  my $vpc_id = $options->{command_args}->[0];

  croak "usage: get_subnets_by_vpc(vpc-id)\n"
    if !$vpc_id;

  my $filter = [
    { Name   => 'vpc-id',
      Values => [$vpc_id]
    }
  ];

  my $subnets = ec2()->DescribeSubnets( { Filters => $filter } );

  return return_output( $options, $subnets );
}

########################################################################
sub get_vpc_by_tag {
########################################################################
  my ($options) = @_;

  my $key   = $options->{command_args}->[0];
  my $value = $options->{command_args}->[1];

  my $vpcs = ec2()->DescribeVpcs;

  foreach my $vpc ( @{ $vpcs->{Vpcs} } ) {
    my $tags = $vpc->{Tags};

    foreach ( @{$tags} ) {
      next if $_->{Key} ne $key;
      next if $_->{Value} ne $value;

      return return_output( $options, $vpc );
    }
  }

  return;
}

########################################################################
sub get_vpc_id {
########################################################################
  my ($options) = @_;

  my $vpc;

  if ( !@{ $options->{command_args} } ) {
    delete $options->{format};
    $vpc = get_default_vpc($options);

    croak "no default vpc\n"
      if !$vpc;
  }
  else {
    $vpc = get_vpc_by_tag($options);

    croak "vpc not found\n"
      if !$vpc;
  }

  return $vpc->{VpcId};
}

# private subnets are tagged as private, however to really get the
# private subnets for a VPC you would need to examine the route tables
# to see if any subnet had a route through an internet gateway (igw-)

########################################################################
sub get_subnets {
########################################################################
  my (%args) = @_;

  my ( $vpc_id, $tag, $key ) = @args{qw(vpc_id tag key)};

  my $subnets = get_subnets_by_vpc( { command_args => [$vpc_id] } );

  my @subnets;

  foreach my $subnet ( @{ $subnets->{Subnets} } ) {

    if ($tag) {
      foreach my $t ( @{ $subnet->{Tags} } ) {
        next if $t->{Key} ne $key;
        next if $t->{Value} !~ /^$tag/xsm;

        push @subnets, $subnet;
      }
    }
    else {
      push @subnets, $subnet;
    }
  }

  return \@subnets;
}

########################################################################
sub get_log_groups {
########################################################################
  my ($options) = @_;

  my $log_groups = logs()->DescribeLogGroups;

  return return_output( $options, $log_groups );
}

########################################################################
sub create_log_group {
########################################################################
  my ($options) = @_;

  my $log_group_name = $options->{log_group};

  if ( !$log_group_name ) {
    $log_group_name = '/ecs/' . $options->{task_name};
  }

  my $log_groups = get_log_groups;

  if ( $log_groups->{logGroups} && @{ $log_groups->{logGroups} } ) {
    my @log_group_names = map { $_->{logGroupName} } @{$log_groups};

    DEBUG sub { return dump_json( \@log_group_names ); };

    if ( none { $log_group_name eq $_ } @log_group_names ) {

      INFO '...creating log group ' . "$log_group_name\n";

      logs()->CreateLogGroup( { logGroupName => $log_group_name } );
    }
    else {
      print {*STDERR} sprintf "%s: log %s already exists\n",
        $options->{command}, $log_group_name;
    }
  }

  return $SUCCESS;
}

########################################################################
sub register_task {
########################################################################
  my ($options) = @_;

  my %args = get_args( $options, 'task_name' );

  my $task_registration_req_file = $args{task_name};

  if ( !$task_registration_req_file ) {
    $task_registration_req_file = sprintf '%s.json', $args{task_name};
  }

  croak 'usage: register_task task-definition-file || task-name'
    if !-e $task_registration_req_file;

  my $task_registration_req = slurp_json($task_registration_req_file);

  my $task_registration_rsp
    = ecs()->RegisterTaskDefinition($task_registration_req);

  return return_output($task_registration_rsp);
}

########################################################################
sub run_task {
########################################################################
  my ($options) = @_;

  my $format = delete $options->{format};

  my $vpc_id = get_vpc_id( { command_args => [ 'Environment', 'sandbox' ] } );

  my $subnets = get_subnets(
    vpc_id => $vpc_id,
    key    => 'Name',
    tag    => 'Private'
  );

  DEBUG sub {
    return Dumper(
      [ vpc_id     => $vpc_id,
        subnets    => $subnets,
        subnet_ids => [ map { $_->{SubnetId} } @{$subnets} ],
      ]
    );
  };

  my %args = get_args( $options, qw(cluster_name task_name) );

  my $task = $args{task_name};

  my $cluster = $args{cluster_name};

  croak 'usage: run-task cluster-name task-name'
    if !$cluster || !$task;

  my $run_task_rsp = ecs()->RunTask(
    { launchType           => 'FARGATE',
      cluster              => $cluster,
      networkConfiguration => {
        awsvpcConfiguration => {
          subnets        => [ map { $_->{SubnetId} } @{$subnets} ],
          assignPublicIp => 'ENABLED'
        },
      },
      taskDefinition => $task,
    }
  );

  print dump_json($run_task_rsp);

  return $SUCCESS;
}

# sets the command args to specified keys
# example: get_args($options, qw(vpc-id key tag))
########################################################################
sub get_args {
########################################################################
  my ( $options, @vars ) = @_;

  return map { @{$_} } zip \@vars, $options->{command_args};
}

########################################################################
sub init_main {
########################################################################
  my ( $options, @option_specs ) = @_;

  GetOptions( $options, @option_specs );

  normalize_options($options);

  Log::Log4perl->get_logger->level( $LOG_LEVELS{ $options->{log_level} } );

  $options->{command} = shift @ARGV // $EMPTY;

  $options->{command_args} = [@ARGV];

  return $options;
}

########################################################################
sub main {
########################################################################
  my $options = init_main( \%DEFAULT_OPTIONS, @OPTION_SPECS );

  TRACE sub {
    return Dumper( [ options => $options ] );
  };

  my $command = $options->{command};

  pod2usage(1)
    if !$command || !defined $COMMANDS{$command};

  return $COMMANDS{$command}->($options);
}

exit main();

__END__

=pod

=head1 NAME

ECS-Examples.pl

=head1 SYNOPSIS

perl ECS-Examples.pl Options Command [Args]

 Options
 -------
 --vpc-name, -v            Name of VPC (default VPC if not provided)
 --image-name, -i          Image name
 --log-group, -l           Log group name (default: /ecs/{task-name})
 --task-name, -t           Task name
 --cluster-name, -c        Cluster name
 --debug, -d,              Enable debug messages
 --task-definition         Task definition file
 --format                  output format, default: json

 Commands           Arguments           Notes
 --------           ---------           -----
 create-log-group
 register-task
 run-task
 get-vpc-by-tag     key value
 get-vpc            [key value]
 get-subnets        vpc-id
 get-default-vpc                        same as get-vpc w/o arguments

 Example:

  perl ECS-Example.pl -c fargate-cluster -t test-amazon-credentials run-task

=back

=head1 DESCRIPTION

Example ECS API calls

=head1 AUTHOR

Rob Lauer - <rlauer6@comcast.net>

=cut
