diff --git a/Web/lib/MJB/Web.pm b/Web/lib/MJB/Web.pm index 62a0ba5..55f0f40 100644 --- a/Web/lib/MJB/Web.pm +++ b/Web/lib/MJB/Web.pm @@ -2,6 +2,7 @@ package MJB::Web; use Mojo::Base 'Mojolicious', -signatures; use MJB::DB; use MJB::Backend::Jekyll; +use MJB::Backend::Nginx; sub startup ($self) { my $config = $self->plugin('NotYAMLConfig', { file => -e 'mjb.yml' @@ -30,6 +31,12 @@ sub startup ($self) { repo => $c->config->{store_repo_base} . "$domain.git", ); }); + + $self->helper( nginx => sub ($c) { + return state $nginx = MJB::Backend::Nginx->new( + servers => [ map { 'root@' . $_->hostname } $c->db->servers->all ], + ); + }); $self->helper( sync_blog => sub ( $c, $blog ) { my $build_job_id = $c->minion->enqueue( 'sync_blog', [ $blog->id ], { @@ -51,14 +58,15 @@ sub startup ($self) { $self->plugin( Minion => { Pg => $self->config->{database}->{minion} } ); # Blog deployment related jobs. - $self->minion->add_task( purge_blog => 'MJB::Web::Task::PurgeBlog' ); - $self->minion->add_task( deploy_blog => 'MJB::Web::Task::DeployBlog' ); - $self->minion->add_task( sync_blog => 'MJB::Web::Task::SyncBlog' ); - $self->minion->add_task( sync_blog_media => 'MJB::Web::Task::SyncBlogMedia' ); + $self->minion->add_task( purge_blog => 'MJB::Web::Task::PurgeBlog' ); + $self->minion->add_task( deploy_blog => 'MJB::Web::Task::DeployBlog' ); + $self->minion->add_task( initialize_blog => 'MJB::Web::Task::InitializeBlog' ); + $self->minion->add_task( sync_blog => 'MJB::Web::Task::SyncBlog' ); + $self->minion->add_task( sync_blog_media => 'MJB::Web::Task::SyncBlogMedia' ); # SSL cert related jobs. - $self->minion->add_task( create_ssl_cert => 'MJB::Web::Task::CreateSSLCert' ); - $self->minion->add_task( sync_ssl_certs => 'MJB::Web::Task::SyncSSLCerts' ); + $self->minion->add_task( create_ssl_cert => 'MJB::Web::Task::CreateSSLCert' ); + $self->minion->add_task( sync_ssl_certs => 'MJB::Web::Task::SyncSSLCerts' ); # Standard router. my $r = $self->routes->under( '/' => sub ($c) { diff --git a/Web/lib/MJB/Web/Controller/Blog.pm b/Web/lib/MJB/Web/Controller/Blog.pm index 3daa2aa..1ac4e9b 100644 --- a/Web/lib/MJB/Web/Controller/Blog.pm +++ b/Web/lib/MJB/Web/Controller/Blog.pm @@ -159,13 +159,19 @@ sub do_settings ( $c ) { $jekyll->config->data->{url} = 'https://' . $blog->domain->name ; $jekyll->write_config; - - my $build_job_id = $c->minion->enqueue( 'deploy_blog', [ $blog->id ], { + + my $build_job_id = $c->minion->enqueue( 'initialize_blog', [ $blog->id ], { notes => { '_bid_' . $blog->id => 1 }, priority => $blog->build_priority, }); $blog->create_related( 'builds', { job_id => $build_job_id } ); + #my $build_job_id = $c->minion->enqueue( 'deploy_blog', [ $blog->id ], { + # notes => { '_bid_' . $blog->id => 1 }, + # priority => $blog->build_priority, + #}); + $blog->create_related( 'builds', { job_id => $build_job_id } ); + $c->flash( confirmation => "Welcome to the dashboard for your new blog!" ); $c->redirect_to( $c->url_for( 'show_dashboard_blog', { id => $blog->id } ) ); } diff --git a/Web/lib/MJB/Web/Task/InitializeBlog.pm b/Web/lib/MJB/Web/Task/InitializeBlog.pm index 8ff52ed..7c50718 100644 --- a/Web/lib/MJB/Web/Task/InitializeBlog.pm +++ b/Web/lib/MJB/Web/Task/InitializeBlog.pm @@ -13,45 +13,14 @@ use IPC::Run3; sub run ( $job, $blog_id ) { $job->note( _mds_template => 'build_static' ); - my $build_dir = $job->checkout_repo( $blog_id ); - my $blog = $job->app->db->blog( $blog_id ); + my $blog = $job->app->db->blog( $blog_id ); + + $job->app->nginx->provision_website( $blog->domain->name ); + + $job->app->sync_blog( $blog ); $job->note( is_clone_complete => 1 ); - # Show the user the commit we're on. - $job->system_command( [ 'git', '-C', $build_dir->child('src')->to_string, 'log', '-1' ] ); - - $build_dir->child('build')->make_path; - - - $job->system_command( [qw( podman run -ti --rm -v .:/srv/jekyll -e JEKYLL_ROOTLESS=1 docker.io/jekyll/jekyll jekyll build ) ], { - chdir => $build_dir->child('src')->to_string, - }); - - $job->process_webroot( - $blog, - $build_dir->child('src')->child('_site')->to_string, - $build_dir->child('build')->to_string - ); - - #== - # Build Site Config - #== TODO: There is two different files made here, one is done by ansible -- pick one, - # probably this one. - Mojo::File->new($build_dir)->child('build')->child('site.yml')->spurt( - YAML::Dump({ - domain => $blog->domain->name, - www_dir => "$build_dir/build/", - }) - ); - - $job->note( is_build_complete => 1 ); - - # Go to the build directory and make $build_dir/. - $ENV{MARKDOWNSITE_CONFIG} = Mojo::File->new($build_dir->to_string)->child('build')->child('site.yml'); - $job->system_command( [ 'ansible-playbook', '/etc/ansible/deploy-website.yml' ] ); - - $job->note( is_deploy_complete => 1 ); $job->finish( ); diff --git a/libs/MJB-Backend-Nginx/dist.ini b/libs/MJB-Backend-Nginx/dist.ini new file mode 100644 index 0000000..1c80f01 --- /dev/null +++ b/libs/MJB-Backend-Nginx/dist.ini @@ -0,0 +1,20 @@ +name = MJB-Backend-Nginx +abstract = Provision websites on nginx servers +author = Kaitlyn Parkhurst +license = Perl_5 +copyright_holder = Kaitlyn Parkhurst +copyright_year = 2022 +version = 0.001 + +[@Basic] + +[Prereqs] +Moo = 0 +IPC::Run3 = 0 +Cwd = 0 +File::Path = 0 +Storable = 0 +Mojo::File = 0 + +[AutoPrereqs] + diff --git a/libs/MJB-Backend-Nginx/lib/MJB/Backend/Nginx.pm b/libs/MJB-Backend-Nginx/lib/MJB/Backend/Nginx.pm new file mode 100644 index 0000000..c45d283 --- /dev/null +++ b/libs/MJB-Backend-Nginx/lib/MJB/Backend/Nginx.pm @@ -0,0 +1,42 @@ +package MJB::Backend::Nginx; +use Moo; +use MJB::Backend::Nginx::DomainConfig; +use Mojo::File qw( tempfile ); + +with 'MJB::Backend::Role::SystemCommand'; + +has servers => ( + is => 'ro', + default => sub { [ ] }, +); + +sub provision_website { + my ( $self, $domain, $ssl_domain ) = @_; + + $ssl_domain ||= $domain; + + my $config = MJB::Backend::Nginx::DomainConfig->new( + domain => $domain, + ssl_domain => $ssl_domain, + )->config; + + my $config_file = tempfile; + $config_file->spurt( $config ); + my $welcome_file = tempfile; + $welcome_file->spurt( "Your new blog is being setup... please reload soon." ); + + foreach my $server ( @{$self->servers} ) { + $self->system_command( [ 'scp', $config_file->to_string, $server . ":/etc/nginx/sites-enabled/" . $domain ] ); + $self->system_command( [ 'ssh', $server, 'mkdir -p /var/www/' . $domain . '/html' ] ); + $self->system_command( [ 'scp', $welcome_file->to_string, $server . "/var/www/" . $domain . "/html/index.html" ] ); + $self->system_command( [ 'ssh', $server, 'chown -R www-data:www-data /var/www/' . $domain ] ); + } +} + +sub deprovision_website { + +} + + + +1; diff --git a/libs/MJB-Backend-Nginx/lib/MJB/Backend/Nginx/DomainConfig.pm b/libs/MJB-Backend-Nginx/lib/MJB/Backend/Nginx/DomainConfig.pm new file mode 100644 index 0000000..17c6c01 --- /dev/null +++ b/libs/MJB-Backend-Nginx/lib/MJB/Backend/Nginx/DomainConfig.pm @@ -0,0 +1,77 @@ +package MJB::Backend::Nginx::DomainConfig; +use Moo; +use Mojo::File; + +has domain => ( + is => 'ro', +); + +has ssl_domain => ( + is => 'ro', +); + +has template => ( + is => 'ro', + default => sub {return <<' EOF;' + server { + server_name {{ domain }}; + root /var/www/{{ domain }}/html; + index index.html; + + error_log /var/log/nginx/{{ domain }}.error.log warn; + access_log /var/log/nginx/{{ domain }}.access.log combined; + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/{{ ssl_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ ssl_domain }}/privkey.pem; + + ssl_session_cache shared:le_nginx_SSL:10m; + ssl_session_timeout 1440m; + ssl_session_tickets off; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; + + ssl_dhparam /etc/nginx/ssl-dhparams.pem; + + } + + server { + if ($host = {{ domain }}) { + return 301 https://$host$request_uri; + } + + listen 80; + server_name {{ domain }} + return 404; + } + EOF; + } +); + +has config => ( + is => 'lazy', +); + +sub _build_config { + my ( $self ) = @_; + + my $config = $self->template; + + # Fill in variables in the template. + my ( $domain, $ssl_domain ) = ( $self->domain, $self->ssl_domain ); + + s/\{\{ domain \}\}/$domain/g, + s/\{\{ ssl_domain \}\}/$ssl_domain/g + for $config; + + # Trim the excess whitespace + $config =~ s/^ {8}//gm; + + + return $config; +} + +1; diff --git a/libs/MJB-Backend-Role-SystemCommand/dist.ini b/libs/MJB-Backend-Role-SystemCommand/dist.ini new file mode 100644 index 0000000..7bb39b9 --- /dev/null +++ b/libs/MJB-Backend-Role-SystemCommand/dist.ini @@ -0,0 +1,20 @@ +name = MJB-Backend-Role-SystemCommand +abstract = Provide system_command as a role +author = Kaitlyn Parkhurst +license = Perl_5 +copyright_holder = Kaitlyn Parkhurst +copyright_year = 2022 +version = 0.001 + +[@Basic] + +[Prereqs] +Moo = 0 +IPC::Run3 = 0 +Cwd = 0 +File::Path = 0 +Storable = 0 +Mojo::File = 0 + +[AutoPrereqs] + diff --git a/libs/MJB-Backend-Role-SystemCommand/lib/MJB/Backend/Role/SystemCommand.pm b/libs/MJB-Backend-Role-SystemCommand/lib/MJB/Backend/Role/SystemCommand.pm new file mode 100644 index 0000000..a659127 --- /dev/null +++ b/libs/MJB-Backend-Role-SystemCommand/lib/MJB/Backend/Role/SystemCommand.pm @@ -0,0 +1,92 @@ +package MJB::Backend::Role::SystemCommand; +use Moo::Role; +use Storable qw( dclone ); +use IPC::Run3 qw( run3 ); + +sub system_command { + my ( $self, $cmd, $settings ) = @_; + + $settings ||= {}; + + # Change the directory, if requested. + if ( $settings->{chdir} ) { + # Throw an error if that directory doesn't exist. + die "Error: directory " . $settings->{chdir} . "doesn't exist." + unless -d $settings->{chdir}; + + # Change to that directory, or die with error. + chdir $settings->{chdir} + or die "Failed to chdir to " . $settings->{chdir} . ": $!"; + + $settings->{return_chdir} = getcwd(); + } + + # Mask values we don't want exposed in the logs. + my $masked_cmd = dclone($cmd); + if ( ref $settings->{mask} eq 'HASH' ) { + foreach my $key ( keys %{$settings->{mask}} ) { + my $value = $settings->{mask}{$key}; + $masked_cmd = [ map { s/\Q$key\E/$value/g; $_ } @{$masked_cmd} ]; + } + } + + # Log the lines + my ( $out, $err ); + my $ret = run3( $cmd, \undef, sub { + chomp $_; + # Mask values we don't want exposed in the logs. + if ( ref $settings->{mask} eq 'HASH' ) { + foreach my $key ( keys %{$settings->{mask}} ) { + my $value = $settings->{mask}{$key}; + s/\Q$key\E/$value/g; + } + } + $out .= "$_\n"; + }, sub { + chomp $_; + # Mask values we don't want exposed in the logs. + if ( ref $settings->{mask} eq 'HASH' ) { + foreach my $key ( keys %{$settings->{mask}} ) { + my $value = $settings->{mask}{$key}; + s/\Q$key\E/$value/g; + } + } + $err .= "$_\n"; + }); + + # Check stderr for errors to fail on. + if ( $settings->{fail_on_stderr} ) { + my @tests = @{$settings->{fail_on_stderr}}; + + while ( my $regex = shift @tests ) { + my $reason = shift @tests; + + if ( $err =~ /$regex/ ) { + die $reason; + } + } + } + + # Return to the directory we started in if we chdir'ed. + if ( $settings->{return_chdir} ) { + chdir $settings->{return_chdir} + or die "Failed to chdir to " . $settings->{chdir} . ": $!"; + } + + if ( $ENV{MJB_DEBUG} ) { + require Data::Dumper; + print Data::Dumper::Dumper({ + stdout => $out, + stderr => $err, + exitno => $ret, + }); + } + + return { + stdout => $out, + stderr => $err, + exitno => $ret, + }; +} + +1;