The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl -w

#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
# vim: set ts=3 sw=3 tw=0:
# vim: set expandtab:

use strict;
use warnings;

$|++;

use LWP::UserAgent;
use YAML;
use Data::Dumper;
use Rex::Config;
use Rex::Logger;
use Rex::Commands::Fs;
use Rex::Commands::File;
use JSON::XS;
use Cwd qw(getcwd);
use Carp;
use URI;
use URI::QueryParam;
use File::Spec;
use File::Basename;
require Rex;

$Rex::Logger::silent = 1;

my ($major_minor) = ($Rex::VERSION =~ m/^(\d*\.\d*)/);
my $opts = {};

######
# default server
my $SEARCH_SERVER      = "http://modules.rexify.org/api/$major_minor/get/recipes";
my $RECIPE_SERVER      = "http://modules.rexify.org/api/$major_minor/get/mod/%s";
my $DEPEND_SERVER      = "http://modules.rexify.org/api/$major_minor/get/dep/%s";
my $PERL_DEPEND_SERVER = "http://modules.rexify.org/api/$major_minor/get/perldep/%s";

my $AUTH_USER;
my $AUTH_PASSWORD;
my $AUTH_REALM;

Rex::Config->register_config_handler(
  "module_server",
  sub {
    my ($param) = @_;

    if ( exists $param->{search} ) {
      $SEARCH_SERVER = $param->{search};
    }

    if ( exists $param->{recipes} ) {
      $RECIPE_SERVER = $param->{recipes};
    }

    if ( exists $param->{dependencies} ) {
      $DEPEND_SERVER = $param->{dependencies};
    }

    if ( exists $param->{perl_dependencies} ) {
      $PERL_DEPEND_SERVER = $param->{perl_dependencies};
    }

    if ( exists $param->{username} ) {
      $AUTH_USER = $param->{username};
    }

    if ( exists $param->{password} ) {
      $AUTH_PASSWORD = $param->{password};
    }

    if ( exists $param->{realm} ) {
      $AUTH_REALM = $param->{realm};
    }

  }
);

for ( my $i = 0 ; $i < @ARGV ; $i++ ) {

  if ( $ARGV[$i] =~ m/^\-\-([a-z0-9\-_]+)=/ ) {
    my $key = $1;
    my ( $c_key, $val ) = split( /=/, $ARGV[$i], 2 );

    if ( exists $opts->{$key} ) {
      $opts->{$key} = [ $opts->{$key} ] if ( !ref $opts->{$key} );
      push( @{ $opts->{$key} }, $val );
    }
    else {
      $opts->{$key} = $val || 0;
    }
  }
  elsif ( $ARGV[$i] =~ m/^\-\-([a-z0-9\-_]+)/ ) {
    my $key = $1;
    if ( !$ARGV[ $i + 1 ] || $ARGV[ $i + 1 ] =~ m/^\-\-/ ) {
      $opts->{$key} = 1;
    }
    else {
      if ( exists $opts->{$key} ) {
        $opts->{$key} = [ $opts->{$key} ] if ( !ref $opts->{$key} );

        push( @{ $opts->{$key} }, $ARGV[ ++$i ] );
      }
      else {
        $opts->{$key} = $ARGV[ ++$i ];
      }
    }
  }

}

if ( !$ARGV[0] || join( "", @ARGV ) =~ m/\-h|\-\-help/ ) {
  print STDERR "Usage: rexify <project-name> [<directory>] [<options>]\n";
  print STDERR "\n";
  print STDERR "Options:";
  print STDERR "\n";
  print STDERR "\t--search=value\t\t\tWill search community recipes\n";
  print STDERR "\t--use=recipe\t\t\tWill download community recipe\n";
  print STDERR
    "\t--init=git-url\t\t\tWill download and initialize the given repo\n";
  print STDERR "\t--update-from-git\t\tUpdate the cloned repository.\n";
  print STDERR
    "\t--template=template\t\tUse a custom template to create the Rexfile skeleton\n";
  print STDERR "\t--create-module\t\t\tCreate a module skeleton.\n";
  print STDERR
    "\t--create-module=Mod::Name\tCreate a module skeleton inside a Rex project.\n";
  print STDERR "\t--sudo\t\t\t\tTo use sudo for Perl Module installation.\n";
  print STDERR
    "\t--resolve-deps\t\t\tRead meta.yml and try to resolve project dependencies\n";
  print STDERR
    "\t--no-install-perl-deps\t\tUse this if you don't want that rexify tries to install Perl Modules.\n";

  print STDERR "\n";
  print STDERR "Custom Templates:\n";
  print STDERR "   box - Template to use for Rex::Commands::Box projects.\n";
  exit 1;
}

sub print_found {
  my ( $name, $data ) = @_;

  $name =~ s/\//::/g;
  $name =~ s/\.pm$//;
  print "* $name\n";
  print "    Author     : " . $data->{Author} . "\n";
  print "    Requires   : " . join( ", ", @{ $data->{Requires} } ) . "\n"
    if ( $data->{Requires} );
  print "    License    : " . $data->{License} . "\n" if ( $data->{License} );
  print "    Description: " . $data->{Description} . "\n";
}

sub update_from_git {
  system "git pull origin";
  resolve_deps("meta.yml");
}

sub download_recipe_git {
  my ($url) = @_;
  $Rex::Logger::silent = 0;

  my $u      = URI->new($url);
  my $branch = $u->query_param("branch");

  my @splitted_path = split /\//, $u->path;
  $splitted_path[-1] =~ s/\.git$//;

  $branch ||= "master";
  my $path = File::Spec->catdir( File::Spec->rel2abs( File::Spec->curdir() ),
    $splitted_path[-1] );

  my $parent_path = dirname $path;
  mkdir $parent_path;

  my $clone_url = $u->scheme . '://' . $u->host . $u->path;

  Rex::Logger::info("Cloning $clone_url to $path. Using branch: $branch.");

  system "git clone $url -b $branch $path";

  chdir $path;
  resolve_deps("meta.yml");
}

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

  if ( $name =~ m/^(https|git):\/\// ) {

    # seems to be a git link
    download_recipe_git($name);
    return;
  }

  if ( !-f "Rexfile" ) {
    print STDERR "This is not a Rex project directory. There is no Rexfile.\n";
    exit 1;
  }

  if ( !-d "lib" ) { mkdir "lib"; }

  print "Getting dependencies for $name...\n";
  my $deps = decode_json( get( sprintf( $DEPEND_SERVER, $name ) ) );
  if ( scalar( @{$deps} ) > 0 ) {
    print "   Found: \n      - " . join( "\n      - ", @{$deps} ) . "\n";
    for my $dep ( @{$deps} ) {
      download_recipe($dep);
    }
  }
  else {
    print "   None found.\n";
  }

  if ( !exists $opts->{"no-install-perl-deps"} ) {
    print "Getting perl dependencies for $name...\n";
    my $perl_deps = decode_json( get( sprintf( $PERL_DEPEND_SERVER, $name ) ) );
    if ( scalar( @{$perl_deps} ) > 0 ) {
      print "   Found: \n      - " . join( "\n      - ", @{$perl_deps} ) . "\n";
      for my $dep ( @{$perl_deps} ) {
        install_perl_module($dep);
      }
    }
    else {
      print "   None found.\n";
    }
  }

  print "Downloading $name...   ";
  $name =~ s/::/\//g;
  my $content = get( sprintf( $RECIPE_SERVER, $name ) );
  open( my $fh, ">", "tmp-mod.tar.gz" ) or die($!);
  binmode $fh;
  print $fh $content;
  close($fh);
  chdir("lib");
  system("tar xvzf ../tmp-mod.tar.gz >dl.log 2>&1");
  unlink("dl.log");
  chdir("..");
  unlink("tmp-mod.tar.gz");

  print "done.\n";

}

sub resolve_deps {
  my ($file) = @_;
  $Rex::Logger::silent = 0;

  $file ||= "meta.yml";

  if ( !-f $file ) {
    confess "No $file file found.";
  }

  my $ref = YAML::LoadFile($file);

  my ( %deps, %perl_deps );

  if ( exists $ref->{Require} ) {
    if ( ref $ref->{Require} eq "ARRAY" ) {
      $deps{$_} = $_ for @{ $ref->{Require} };
    }
    else {
      %deps = %{ $ref->{Require} };
    }
  }

  if ( exists $ref->{PerlRequire} ) {
    if ( ref $ref->{PerlRequire} eq "ARRAY" ) {
      $perl_deps{$_} = $_ for @{ $ref->{PerlRequire} };
    }
    else {
      %perl_deps = %{ $ref->{PerlRequire} };
    }
  }

  Rex::Logger::debug("Found dependencies: ");
  Rex::Logger::debug( Dumper( \%deps ) );

  for my $req ( keys %deps ) {
    if ( ref $deps{$req} ) {
      if ( exists $deps{$req}->{git} ) {

        # git dep
        my $branch = "master";
        if ( exists $deps{$req}->{branch} ) {
          $branch = $deps{$req}->{branch};
        }

        my @path_parts = split /::/, $req;
        my $path =
          File::Spec->catdir( File::Spec->rel2abs( File::Spec->curdir() ),
          "lib", @path_parts );

        my $parent_path = dirname $path;
        if ( !-d $parent_path ) {
          mkdir $parent_path;
        }

        if ( -d $path && !-d "$path/.git" ) {
          Rex::Logger::info( "$req not under git control. Skipping.", "warn" );
          next;
        }

        if ( -d "$path/.git" ) {
          system "rm -rf $path";
        }

        Rex::Logger::info("Cloning $deps{$req}->{git}#$branch to $path");
        system "git clone $deps{$req}->{git} -b $branch $path";
        resolve_deps("$path/meta.yml");
      }
    }
    else {
      download_recipe($req);
    }
  }

  Rex::Logger::debug("Found perl dependencies: ");
  Rex::Logger::debug( Dumper( \%perl_deps ) );

  for my $req ( keys %perl_deps ) {
    if ( ref $perl_deps{$req} ) {
      if ( exists $perl_deps{$req}->{git} ) {

        # git dep
        my $branch = "master";
        if ( exists $perl_deps{$req}->{branch} ) {
          $branch = $perl_deps{$req}->{branch};
        }

        my $curdir = getcwd;
        my $path = File::Spec->catdir( File::Spec->tmpdir, "tmp-build-$$" );

        my $lib_path =
          File::Spec->catdir( File::Spec->rel2abs( File::Spec->curdir() ),
          "lib", "perl" );

        my $parent_path = dirname $lib_path;
        if ( !-d $parent_path ) {
          mkdir $parent_path;
        }

        Rex::Logger::info("Cloning $perl_deps{$req}->{git}#$branch to $path");
        system "git clone $perl_deps{$req}->{git} -b $branch $path";

        chdir $path;
        system "cpanm -l $lib_path -L $lib_path .";
        chdir $curdir;

        system "rm -rf $path";
      }
    }
    else {
      # we need relative directories, because auf a cpanm bug on windows.
      my $lib_path = File::Spec->catdir( File::Spec->curdir(), "lib", "perl" );

      system "cpanm -l $lib_path -L $lib_path $req";
    }
  }

}

sub install_perl_module {
  my ($mod) = @_;

  print "Checking $mod: ";
  eval "use $mod";
  if ($@) {
    print "[failed]\n";
  }
  else {
    print "[ok]\n";
    return;
  }

  print "Trying to install $mod... ";

  my $cmd = "cpanm";
  my $out = qx($cmd --help 2>&1);
  if ( $? != 0 ) {
    $cmd = "cpan";
    $out = qx($cmd -h 2>&1);

    if ( $? != 0 ) {
      print "[failed]\n";
      print "Can't find cpanm or cpan. Please install $mod manually.\n";
      return;
    }

  }

  my $cpanm_opts = "";
  if ( exists $opts->{sudo} ) {
    $cmd = "sudo $cmd";
  }

  $out = qx($cmd $cpanm_opts $mod 2>&1);
  open( my $log, ">>", "rexify-install.log" ) or die($!);
  print $log $out;
  close($log);
  if ( $? != 0 ) {
    print "[failed]\n";
    print
      "!! Please install $mod manually. See rexify-install.log for more details.\n";
  }
  else {
    print "[ok]\n";
  }
}

if ( exists $opts->{search} ) {
  my $search_string = $opts->{search};

  # only a search
  print "Downloading recipes.yml ... ";
  my $recipes = get($SEARCH_SERVER);
  print " done.\n";

  print "Searching...\n\n";

  my $data = Load($recipes);

  for my $mod ( keys %{$data} ) {
    if ( $mod =~ qr{$search_string}i ) {
      print_found( $mod, $data->{$mod} );
      next;
    }

    if ( $data->{$mod}->{Description} =~ qr{$search_string}i ) {
      print_found( $mod, $data->{$mod} );
    }
  }

  exit 0;
}
if ( exists $opts->{"update-from-git"} ) {
  update_from_git();
  exit 0;
}

if ( exists $opts->{init} ) {
  download_recipe_git( $opts->{init} );
  exit 0;
}

if ( exists $opts->{use} && $ARGV[0] =~ /^\-\-use/ ) {
  if ( $opts->{use} ) {
    if ( !ref $opts->{use} ) {
      $opts->{use} = [ $opts->{use} ];
    }

    for my $use_mod ( @{ $opts->{use} } ) {
      download_recipe($use_mod);
    }
  }

  exit 0;
}
if ( exists $opts->{"resolve-deps"} && $opts->{"resolve-deps"} ) {
  resolve_deps("meta.yml");
  exit 0;
}

if ( exists $opts->{"create-module"} ) {
  my $dir         = $ARGV[0];
  my $module_name = $dir;

  if ( $dir !~ m/^[a-zA-Z][a-zA-Z_:0-9]+$/ ) {
    print "USAGE: $0 Module::Name --create-module\n";
    print "       Allowed characters: a-z, A-Z, _, 0-9, '::'.\n";
    exit 1;
  }

  if ( -f "Rexfile" && $opts->{"create-module"} ) {
    $dir = "lib/$dir";
  }

  if ( $dir =~ m/\-\-create\-module/ ) {
    print "USAGE: $0 Module::Name --create-module\n";
    print "       Allowed characters: a-z, A-Z, _, 0-9, '::'.\n";
    exit 1;
  }

  $dir =~ s/::/\//g;

  print "Creating module $module_name...\n";

  print "   mkdir $dir\n";
  mkdir $dir, recursive => 1;
  chdir $dir;

  print "   Creating template file: __module__.pm\n";
  file "__module__.pm",
    content =>
    template( '@module.pm.tpl', dir => $dir, module_name => $module_name );

  print "   Creating template file: meta.yml\n";
  file "meta.yml",
    content =>
    template( '@meta.yml.tpl', dir => $dir, module_name => $module_name );

  print "\n";
  print "Your module has been created in $dir.\n";

  exit 0;
}

my $dir = $ARGV[0];

if ( defined $ARGV[1] && $ARGV[1] !~ m/^\-\-/ ) {
  $dir = $ARGV[1];
}

if ( $dir !~ m/^[a-zA-Z][a-zA-Z_:0-9]+$/ ) {
  print "USAGE: $0 Project::Name\n";
  print "       Allowed characters: a-z, A-Z, 0-9, _, '::'.\n";
  exit 1;
}

if ( exists $opts->{template}
  && -f $opts->{template}
  && $opts->{template} !~ m/^\// )
{
  my $cwd = getcwd;
  $opts->{template} = "$cwd/" . $opts->{template};
}
elsif ( exists $opts->{template} && $opts->{template} !~ m/^\// ) {
  $opts->{template} = '@' . $opts->{template};
}

unless ( -d $dir ) {
  print "Created $dir\n";
  mkdir($dir);
}
print "chdir to $dir\n";
chdir($dir);

unless ( -d 'lib' ) {
  mkdir('lib');
}

unless ( -f 'lib' . $ARGV[0] . '.pm' ) {
  open( my $fh, ">", "lib/$ARGV[0].pm" ) or die($!);
  print $fh template( '@libfile', lib => $ARGV[0] );
  close($fh);

  print STDERR "Created lib/Rex/$ARGV[0].pm\n";

  if ( $opts->{template} ) {
    open( $fh, ">", "Rexfile" ) or die($!);
    print $fh template( $opts->{template}, lib => $ARGV[0] );
    close($fh);
  }
  else {
    open( $fh, ">", "Rexfile" ) or die($!);
    print $fh template( '@rexfile', lib => $ARGV[0] );
    close($fh);
  }

  if ( $opts->{use} ) {
    if ( !ref $opts->{use} ) {
      $opts->{use} = [ $opts->{use} ];
    }

    for my $use_mod ( @{ $opts->{use} } ) {
      download_recipe($use_mod);
    }
  }

  print STDERR "Created Rexfile.\n";
  print STDERR "Done.\n\nNow edit Rexfile to suite your needs.\n";
  print STDERR "You can edit $dir/lib/$ARGV[0].pm to define tasks.\n";
  print STDERR
    "\n\nIf you have any questions or wishes\n\n\tjust join #rex on freenode\n\nor post them here:\n\n\thttps://github.com/RexOps/Rex/issues\n\n";
}
else {

  if ( $opts->{use} ) {
    if ( !ref $opts->{use} ) {
      $opts->{use} = [ $opts->{use} ];
    }

    for my $use_mod ( @{ $opts->{use} } ) {
      download_recipe($use_mod);
    }
  }

  exit;
}

sub get {
  my ($url) = @_;

  my $ua = LWP::UserAgent->new;
  $ua->env_proxy;

  if ( $AUTH_USER && $AUTH_PASSWORD ) {
    my ($netloc) = ( $RECIPE_SERVER =~ m/^https?:\/\/([^\/]+)\// );
    unless ( $netloc =~ m/\:\d+$/ ) {
      if ( $netloc =~ m/^https/ ) {
        $netloc .= ":443";
      }
      else {
        $netloc .= ":80";
      }
    }
    $ua->credentials( $netloc, $AUTH_REALM, $AUTH_USER, $AUTH_PASSWORD );
  }

  my $resp = $ua->get($url);
  if ( $resp->is_success ) {
    return $resp->decoded_content;
  }
}

__DATA__

@rexfile
# enable new Features
use Rex -feature => 0.40;

# set your username
set user => "<user>";

# set your password
set password => "<password>";

# enable password authentication
set -passauth;

# put your server in this group
set group => "servers" => "server1", "server2";


# now load every module via ,,require''
require <%= $::lib %>;

@end

@libfile
package <%= $::lib %>;

use Rex -base;

desc "Get uptime of server";
task "uptime", group => 'servers', sub {
   say run "uptime";
};

1;
@end

@box
use strict;
use warnings;

use Rex -feature => 0.40;
use Rex::Commands::Box;

set user => '<user>';
set password => '<password>';
set -passauth;

#
# CALL:
# rex init --name=<%= $::lib %> --url=http://box.rexify.org/box/ubuntu-server-12.10-amd64.ova
desc "Initialize and start the VM: rex init --name=vmname [--url=http://...]";
task "init", sub {

   my $param = shift;

   box {
      my ($box) = @_;
      $box->name($param->{name});

      # where to download the base image
      $box->url($param->{url});

      # default is nat
      #$box->network(1 => {
      #   type => "bridged",
      #   bridge => "eth0",
      #});

      # only works with network type = nat
      # if a key ssh is present, rex will use this to log into the vm
      # you need this if you run VirtualBox not on your local host.
      $box->forward_port(ssh => [2222 => 22]);

      # share a folder from the host system
      #$box->share_folder("sharename" => "/path/on/host/system");

      # define the authentication to the box
      # if you're downloading one from box.rexify.org this is the default.
      $box->auth(
         user => "root",
         password => "box",
      );

      # if you want to provision the machine,
      # you can define the tasks to do that
      $box->setup(qw/install_webserver/);
   };

};

#
# CALL:
# rex down --name=<%= $::lib %>
desc "Stop the VM: rex down --name=vmname";
task "down", sub {

   my $param = shift;

   my $box = Rex::Commands::Box->new(name => $param->{name});
   $box->stop;
};


task "install_webserver", sub {

   # update package db
   update_package_db;

   # install packages / customize the vm
   install "apache2";

};

require <%= $::lib %>;

@end

@module.pm.tpl
package <%= $::module_name %>;

use Rex -base;

task example => sub {
   my $output = run "uptime";
   say $output;
};

1;

=pod

=head1 NAME

$::module_name - {{ SHORT DESCRIPTION }}

=head1 DESCRIPTION

{{ LONG DESCRIPTION }}

=head1 USAGE

{{ USAGE DESCRIPTION }}

 include qw/<%= $::module_name %>/;

 task yourtask => sub {
    <%= $::module_name %>::example();
 };

=head1 TASKS

=over 4

=item example

This is an example Task. This task just output's the uptime of the system.

=back

=cut

@end

@meta.yml.tpl
Name: <%= $::module_name %>
Description: {{ DESCRIPTION }}
Author: {{ your name <your.name@email.com> }}
License: {{ THE LICENSE }}

# Only if you have dependencies to other Rex Modules.
Require:
   - Other::Rex::Module
   - 2nd::Rex::Module
@end