Group
Extension

Dancer2-Plugin-Etcd/lib/Dancer2/Plugin/Etcd/CLI.pm

package Dancer2::Plugin::Etcd::CLI;

use utf8;
use strict;
use warnings;

=head1 NAME

Dancer2::Plugin::Etcd::CLI

=cut

our $VERSION = '0.011';

use Dancer2::Core::Runner;
use Dancer2::FileUtils qw/dirname path/;
use Sys::Hostname;
use File::Spec qw/catfile/;
use Getopt::Long;
use Path::Tiny;
use Hash::Flatten;
use Cwd;
use Net::Etcd;
use JSON;
use YAML::Syck;
use Path::Class qw( file );
use File::Spec;
use Try::Tiny;
use Data::Dumper;

use constant { SUCCESS => 0, INFO => 1, WARN => 2, ERROR => 3 };

use Class::Tiny;

our $UseSystem = 0; # 1 for unit testing

{
    package ReadConfig;
    use Moo;
    with 'Dancer2::Core::Role::ConfigReader';

    has '+location' => (
        is => 'ro'
    );

    has '+environment' => (
        is => 'ro'
    );
}

=head2 run

=cut

sub run {
    my($self, @args) = @_;

    my @commands;
    my $p = Getopt::Long::Parser->new(
        config => [ "no_ignore_case", "pass_through" ],
    );
    $p->getoptionsfromarray(
        \@args,
        "h|help"    => sub { unshift @commands, 'help' },
        "v|version" => sub { unshift @commands, 'version' },
        "verbose!"  => sub { $self->verbose($_[1]) },
    );

    push @commands, @args;

    my $cmd = shift @commands || 'help';

    my $code = try {
        my $call = $self->can("cmd_$cmd")
            or die "Could not find command '$cmd'";
        $self->$call(@commands);
        return 0;
    } catch {
        die $_ ;
    };

    return $code;
}

=head2 commands

=cut

sub commands {
    my $self = shift;

    no strict 'refs';
    map { s/^cmd_//; $_ }
        grep { /^cmd_.*/ && $self->can($_) } sort keys %{__PACKAGE__."::"};
}

=head2 cmd_help

=cut

sub cmd_help {
    my $self = shift;
    $self->print(<<HELP);
Usage: shepherd <command>
where <command> is one of:
  @{[ join ", ", $self->commands ]}
Options:
--env       Define current running environment.
            Note: Default is development.
--apphost   The hostname is the domain related to the config.
--appname   The name of the Dancer app.
--apppath   Path to app
--version   By default we use the last version.  This flag allows selection of specific version.
--etcdhost  Etcd host
--etcdssl   Etcd ssl connection
--etcdport  Etcd port
--etcduser  Etcd user
--etcdpass  Etcd user password
--readonly  Only print the new configs to screen do no replace current.
--help      This help screen

Run shepherd -h <command> for help.
HELP
}

=head2 parse_options

=cut

sub parse_options {
    my($self, $args, @spec) = @_;
    my $p = Getopt::Long::Parser->new(
        config => [ "no_auto_abbrev", "no_ignore_case" ],
    );
    $p->getoptionsfromarray($args, @spec);
}

=head2 parse_options_pass_through

=cut

sub parse_options_pass_through {
    my($self, $args, @spec) = @_;

    my $p = Getopt::Long::Parser->new(
        config => [ "no_auto_abbrev", "no_ignore_case", "pass_through" ],
    );
    $p->getoptionsfromarray($args, @spec);

    # with pass_through keeps -- in args
    shift @$args if $args->[0] && $args->[0] eq '--';
}

=head2 printf

=cut

sub printf {
    my $self = shift;
    my $type = pop;
    my($temp, @args) = @_;
    $self->print(sprintf($temp, @args), $type);
}

=head2 print

=cut

sub print {
    my($self, $msg, $type) = @_;
    my $fh = $type && $type >= WARN ? *STDERR : *STDOUT;
    print {$fh} $msg;
}

=head2 cmd_get

=cut

sub cmd_get{
    my($self, @args) = @_;

    my($env, $version, $app_path, $app_name, $app_host, $readonly, $etcd_host,
      $etcd_ssl, $etcd_port, $etcd_user, $etcd_pass);

    $self->parse_options(
        \@args,
        "e|env=s"      => \$env,
        "v|version=i"  => \$version,
        "h|apphost=s"  => \$app_host,
        "p|apppath=s"  => \$app_path,
        "n|appname=s"  => \$app_name,
        "etcdhost=s"   => \$etcd_host,
        "etcdssl=s"    => \$etcd_ssl,
        "etcdport=s"   => \$etcd_port,
        "etcduser=s"   => \$etcd_user,
        "etcdpass=s"   => \$etcd_pass,
        "readonly!"    => \$readonly,
    );

    $env    ||= 'development';
    $app_path ||= getcwd;
    $app_host ||= hostname;

    my $app = ReadConfig->new( location => $app_path, environment => $env );
    my $files = $app->config_files;

    #TODO this is a nasty hack
    my $base_conf = file( File::Spec->rel2abs($files->[0]) );
    my $base_conf_data = LoadFile($base_conf);

    my $settings = $base_conf_data->{plugins}{Etcd};

    # check for plugin conf

    die "This command must be run from the base dir of a dancer app.\n" unless (@$files);

    $settings->{name} = $etcd_user if $etcd_user;
    $settings->{password} = $etcd_pass if $etcd_pass;
    $settings->{ssl} = $etcd_ssl if $etcd_ssl;
    $settings->{port} = $etcd_port if $etcd_port;
    $settings->{host} = $etcd_host if $etcd_host;

    die "You must pass the etcd name, password, host and port.\n" unless $settings;

    my $etcd = Net::Etcd->new(\%$settings);

	#print STDERR Dumper($etcd);

    for my $file (@$files) {
        my $conf_path = file( File::Spec->rel2abs($file) );

        my $conf_data = LoadFile($conf_path);

        my $test = $conf_data->{'plugins'}{'Etcd'};

        unless ($app_name) {
            $app_name = $conf_data->{'appname'} if $conf_data->{'appname'};
        }

        my $env_path = File::Spec->catdir( $app_name, $app_host, $env);
        my $key_path = File::Spec->catdir( $env_path, $conf_path->relative );
        my $version = $version ? sprintf("%08d", $version) : $etcd->range({ key => "/$env_path/version" })->get_value;

        my $input = $etcd->range({ key => "/$key_path/$version/00000000", range_end => "/$key_path/$version/99999999"});
        my @range = @{$input->all};
        die "No confiuration exists for this version." unless @range;

        my $line_hash;
        for my $row (@range) {
            my $key = $row->{key};
            my $value = $row->{value};

            # print " key: $row, value: $value";
            $key =~ s/\/.+[a-zA-Z]\/\d+//;
            $value =~ s/\\n//;
            $value =~ s/\\('|")/$1/g;
            $line_hash->{$key} = $value;
        }

        my $o = Hash::Flatten->new({
                    HashDelimiter => '/',
                    ArrayDelimiter => '/-/',
                    OnRefScalar => 'warn',
        });

        my $config_file = path($conf_path->relative);
        my @output;
        #TODO need to clean this up into a function and make it scalable.
        my $flat = $o->unflatten($line_hash);
        foreach my $row (keys %$flat) {
            foreach my $line (sort keys %{ $flat->{$row} }) {
                foreach my $indent (keys %{ $flat->{$row}{$line} }) {
                   foreach my $config (keys %{ $flat->{$row}{$line}{$indent} }) {
                        if (ref($flat->{$row}{$line}{$indent}{$config}) ne 'HASH') {
                            push @output, $flat->{$row}{$line}{$indent}{$config} . "\n";
                        }
                        else {
                            push @output, sprintf "%-*s%s", $indent, '', $config;
                        }
                        no strict; #FIXME
                        foreach my $value (keys %{ $flat->{$row}{$line}{$indent}{$config}}) {
                            if (ref($flat->{$row}{$line}{$indent}{$config}{$value}) ne 'HASH') {
                                my $cvalue = $flat->{$row}{$line}{$indent}{$config}{$value};
                                push @output, $cvalue ? ": $flat->{$row}{$line}{$indent}{$config}{$value}\n" : ":\n";
                            }
                            else{
                                 foreach my $avalue (keys %{ $flat->{$row}{$line}{$indent}{$config}{$value}}) {
                                     push @output, " $flat->{$row}{$line}{$indent}{$config}{$value}{$avalue}\n";
                                 }
                            }
                        }
                    }
                }
            }
        }
        $readonly ? print "@output\n" : $config_file->spew_utf8( @output );
        $self->print("Complete! Config $file saved.\n", SUCCESS);
    }
}

=head2 cmd_put

=cut

sub cmd_put{
    my($self, @args) = @_;

    my($env, $app_path, $app_name, $app_host, $readonly, $etcd_host,
      $etcd_ssl, $etcd_port, $etcd_user, $etcd_pass, @configs);

    $self->parse_options(
        \@args,
        "e|env=s"      => \$env,
        "h|apphost=s"  => \$app_host,
        "p|apppath=s"  => \$app_path,
        "n|appname=s"  => \$app_name,
        "etcdhost=s"   => \$etcd_host,
        "etcdssl=s"    => \$etcd_ssl,
        "etcdport=s"   => \$etcd_port,
        "etcduser=s"   => \$etcd_user,
        "etcdpass=s"   => \$etcd_pass,
        "readonly!"    => \$readonly,
    );

    $env    ||= 'development';
    $app_path ||= getcwd;
    $app_host ||= hostname;

    my $app = ReadConfig->new( location => $app_path, environment => $env );
    my $files = $app->config_files;

    #TODO this is a nasty hack
    my $base_conf = file( File::Spec->rel2abs($files->[0]) );
    my $base_conf_data = LoadFile($base_conf);

    my $settings = $base_conf_data->{plugins}{Etcd};

    # check for plugin conf

    die "This command must be run from the base dir of a dancer app.\n" unless (@$files);

    $settings->{name} = $etcd_user if $etcd_user;
    $settings->{password} = $etcd_pass if $etcd_pass;
    $settings->{ssl} = $etcd_ssl if $etcd_ssl;
    $settings->{port} = $etcd_port if $etcd_port;
    $settings->{host} = $etcd_host if $etcd_host;

    die "You must pass the etcd name, password, host and port.\n" unless $settings;

    my $etcd = Net::Etcd->new(\%$settings);

    for my $file (@$files) {
        my $conf_path = file( File::Spec->rel2abs($file) );

        print 'backing up ' . $file . "\n";

        # to properly define the yaml we load raw data
        my @lines = path($conf_path)->lines_utf8;

        # we also use the hashref to get data like
        my $conf_data = LoadFile($conf_path);

        unless ($app_name) {
            $app_name = $conf_data->{'appname'} if $conf_data->{'appname'};
        }


        my $output;
        my $line_count = 0;

        # format data for etcd
        for my $out (@lines) {
            $line_count++;
            my $ln = sprintf("%08d", $line_count);
            $out =~ /^( *)/;
            my $count = length( $1 );
            # save comments
            if ($out =~ s/(#.*)//){
               $output->{$ln}{$count} = $1;
            }
            else {
                # escape quotes in values
                $out =~ s/(:*'|")/\\$1/g;
                # handle unquoted zero
                $out =~ s/(.*: *)([0])/$1\\'$2\\'/g;
                $output->{$ln}{$count} = Load($out);
            }
        }
        my $o = Hash::Flatten->new({
                    HashDelimiter => '/',
                    ArrayDelimiter => '/-/',
                    OnRefScalar => 'warn',
        });

        my $flat_conf_data = $o->flatten($output);
        push @configs, { path => $conf_path->relative, environment => $env,
          data => $flat_conf_data, line_count => $line_count };
    }

 
    my $env_path = File::Spec->catdir( $app_name, $app_host, $env);
    my $version = $etcd->range({ key => "/$env_path/version" })->get_value || sprintf("%08d", 0);
    $version++;

    for my $config (@configs) {
        die "The AppName must be set." unless $app_name;
        my $data = $config->{data};
        my $key_path = File::Spec->catdir($env_path, $config->{path});

        for my $key ( keys %$data ) {
            my $value = $data->{$key} || '\n';
            #print "/$key_path/$version/$key/, $value\n";
            $etcd->put( { key => "/$key_path/$version/$key/", value => $value });
        } 
    }
    print "latest version is now: $env_path:v" . sprintf("%d\n",$version);
    $etcd->put({ key => "/$env_path/version", value =>  sprintf("%08d", $version) });
}

1;


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