#!/usr/bin/perl
#
# puppetca-backend -- Puppet CA remote administration wrapper.
#
# Written by Digant C Kasundra <digant@stanford.edu>
# Modified by Adam H. Lewenberg <adamhl@stanford.edu>
# Copyright 2007, 2010, 2013, 2017, 2021 Board of Trustees, Leland Stanford Jr. University

## no critic (CodeLayout::ProhibitParensWithBuiltins);
## no critic (InputOutput::RequireBracedFileHandleWithPrint);

use strict ;
use warnings ;
use autodie ;

use Getopt::Long::Descriptive ;
use Carp ;
use IPC::Cmd ;
use Pod::Usage ;
use Sys::Hostname;

my $VERBOSE = 0 ;
my $KUBECTL = 0 ;
my $HOSTNAME = get_hostname() ;

sub progress {
    my ($msg) = @_ ;

    if ($VERBOSE) {
        print "progress: $msg\n" ;
    }
    return ;
}

# Print the help information.
sub print_help {
    my $help_string = <<~'EOP';
       Description:
           The Puppet Cert Authority remctl scripts allow for ease of
           administration by enabling certain work to be done remotely.

       Usage:
           remctl puppetca pca <command> [options]

       Commands:
           help
           manual
           list
           list-unsigned (same as list)
           list-all
           sign   <fully-qualified hostname>
           revoke <fully-qualified hostname>
           clean  <fully-qualified hostname>
           inventory (same as list-all)
       EOP

    print $help_string;
    return ;
}

sub get_hostname {
    my $hn ;
    if (exists($ENV{'HOSTNAME'})) {
        progress('getting hostname from HOSTNAME environment variable') ;
        $hn = $ENV{'HOSTNAME'} ;
    } else {
        progress('getting hostname from Perl hostname module') ;
        $hn = hostname() ;
    }

    progress("hostname is '$hn'") ;
    return $hn ;
}

#<<<  perltiudy ignore this section as I like the options formatted this way.
my ($opt, $usage) = describe_options(
    'puppetca-backend %o ((sign|revoke|clean) <hostname>)|list-signed|list-unsigned|list-all',
    [ 'kubectl|k', 'use kubectl interface' ],
    [ 'manual|m',  'print manual page and exit', {'shortcircuit' => 1} ],
    [ 'verbose|v', 'print extra stuff'            ],
    [ 'help|h',    'print usage message and exit', {'shortcircuit' => 1}],
    );
#>>>

if ($opt->manual) {
    pod2usage(-verbose => 2) ;
    exit 0 ;
}

if ($opt->help()) {
    print $usage->text ;
    exit ;
}

$VERBOSE = $opt->verbose() ;
$KUBECTL = $opt->kubectl() ;

# Parse command-line arguments.
my ($ACTION, $SYSTEM) = @ARGV;
if (!$ACTION) {
    print_help;
    exit 1;
} else {
    progress("action is '$ACTION'") ;
}

if ($SYSTEM) {
    progress("node name is '$SYSTEM'") ;

    # Only allow system names with word characters.
    check_system_name($SYSTEM) ;
}

my %ALLOWED_ACTIONS = (
    'help'          => \&help,
    'manual'        => \&manual,
    'list'          => \&list_unsigned,
    'list-unsigned' => \&list_unsigned,
    'list-all'      => \&list_all,
    'inventory'     => \&list_all,
    'clean'         => \&clean,
    'revoke'        => \&revoke,
    'sign'          => \&sign,
    ) ;

my @puppetca = qw ( puppetserver ca ) ;

# Call the action.
if (exists($ALLOWED_ACTIONS{$ACTION})) {
    $ALLOWED_ACTIONS{$ACTION}->() ;
    exit 0 ;
} else {
    croak "Syntax error or unknown command\n";
}

##################################################
sub exit_with_error {
    my ($msg) = @_ ;
    print "error: $msg\n" ;
    exit 1 ;
}

sub help {
    print_help() ;
    return ;
}

sub manual {
    pod2usage(-verbose => 2) ;
    return ;
}

sub exec_ca_command {
    my (@cmd) = @_ ;

    if ($KUBECTL) {
        progress('using kubectl');
        return exec_kubectl(@cmd) ;
    } else {
        progress('using direct command');
        return exec_direct(@cmd) ;
    }
}

sub exec_kubectl {
    my (@cmd) = @_ ;

    my @command = (
        'kubectl',
        'exec',
        $HOSTNAME,
        '--container',
        'puppetserver',
        q{--},
        'puppetserver',
        'ca',
        @cmd,
        ) ;

    return run_command(@command) ;
}

sub exec_direct {
    my (@cmd) = @_ ;

    my @command = (
        'puppetserver',
        'ca',
        @cmd,
        ) ;

    return run_command(@command) ;
}


sub list_unsigned {
    my @cmd = qw(list) ;
    my ($stdout, $stderr, $rc, $err) = exec_ca_command(@cmd) ;
    print $stdout ;
    return ;
}

sub list_all {
    my @cmd = qw(list --all) ;
    my ($stdout, $stderr, $rc, $err) = exec_ca_command(@cmd) ;
    print $stdout ;
    return ;
}

sub action {
    my ($action) = @_ ;

    my @cmd = ($action, '--certname', $SYSTEM) ;
    my ($stdout, $stderr, $rc, $err) = exec_ca_command(@cmd) ;
    print $stdout ;
    return ;
}

sub clean {
    return action('clean') ;
}

sub revoke {
    return action('revoke') ;
}

sub sign {
    return action('sign') ;
}

sub check_system_name {
    my ($name) = @_ ;

    my $msg ;
    if (!$name) {
        $msg = q{missing system name} . "\n" ;
    } elsif ($name !~ m{^[-.\w]+$}ixsm) {
        # Allow only word characters (a-Z0-9_) and dash ("-") in name.
        $msg = q{system name contains invalid characters} ;
    } elsif ($name !~ m{^[^.]+[.][^.]+}ixsm) {
        $msg = q{system name must be fully qualified }
             . q{(e.g., 'example.stanford.edu')} . "\n" ;
    }

    if ($msg) {
        exit_with_error($msg) ;
    }

    return ;
}

# Use: pass in an array, returns ($stdout, $stderr, $exit_value)
sub run_command {
    my (@command) = @_ ;

    # Try to use IPC::Run. If that is not available, try using
    # IPC::Open3.
    $IPC::Cmd::USE_IPC_OPEN3 = 1 ;

    progress(q{about to run command '} . join(q{ }, @command) . q{'}) ;

    my ($ok, $err, $full_buf, $stdout_buffer, $stderr_buffer) =
      IPC::Cmd::run(command => \@command) ;

    # Convert buffers to strings.
    my $stdout = join("\n", @{$stdout_buffer}) ;
    my $stderr = join("\n", @{$stderr_buffer}) ;

    # Try and extract error.
    my $return_code = 0 ;
    if ($err && ($err =~ m{exited[ ]with[ ]value[ ](\d+)$}xsm)) {
        $return_code = $1 ;
    }

    $stdout = $stdout ? $stdout : q{} ;
    $stderr = $stderr ? $stderr : q{} ;
    $err    = $err    ? $err    : q{} ;

    progress("stdout:    [$stdout]") ;
    progress("stderr:    [$stderr]") ;
    progress("exit code: [$return_code]") ;
    progress("err:       [$err]") ;

    if ($return_code != 0) {
        exit_with_error($stderr) ;
    }

    return ($stdout, $stderr, $return_code, $err) ;
}

__END__

=head1 NAME

puppetca-backend - remctl interface for the Puppet Certificate Authority

=head1 SYNOPSIS

B<puppetca-backend> --help|-h

B<puppetca-backend> --manual|-m

B<puppetca-backend> [-v] [-k] ( sign | revoke | clean ) I<hostname>

B<puppetca-backend> [-v] [-k] ( list-signed | list-unsigned | list-all )

=head1 DESCRIPTION

This is a Perl wrapper script for some operations and tools useful for
maintaining the puppet cert authority. Use the C<-k> option when running
in a Kubernetes context.

=head1 ACTIONS

=over 4

=item list-unsigned

List of certificates waiting to be signed.

=item list-all

List of all certificates (signed, unsigned, revoked, etc).

=item sign I<hostname>

Sign certificate with server name I<hostname>.

=item revoke I<hostname>

Revoke the certificate of a client.

=item clean I<hostname>

Revoke a host's certificate (if applicable) and remove all files
related to that host from puppet cert's storage.

=back

=head1 OPTIONS

=over 4

=item B<--kubectl|-k>

Use kubetctl rather than running the commands directly. Thus,
C<puppetca-backend -k list> is equivalent to
C<kubectl exec $HOSTNAME -- puppetserver ca list --all>.

=back

=head1 AUTHOR

Digant C Kasundra <digant@stanford.edu>

Updated by Adam H. Lewenberg <adamhl@stanford.edu>

=head1 COPYRIGHT

Copyright 2007, 2010, 2017, 2021 Board of Trustees, Leland Stanford Jr. University.

=cut
