Compare commits
9 commits
master
...
feat-nginx
Author | SHA1 | Date | |
---|---|---|---|
ca6bae8463 | |||
ad0c7ae94d | |||
e7068cfc7c | |||
62278abede | |||
1e5a3d57b5 | |||
e9237d3586 | |||
efdff0bc26 | |||
2401d40047 | |||
bfaf077c02 |
11 changed files with 1500 additions and 9 deletions
|
@ -1,10 +1,16 @@
|
|||
pipeline:
|
||||
backend:
|
||||
image: rust
|
||||
# environment:
|
||||
# - DATABASE_URL=postgres://postgres:password@database:5432/postgres
|
||||
commands:
|
||||
# - make migrate
|
||||
- apt update
|
||||
- apt-get install -y --no-install-recommends nginx sudo
|
||||
- mkdir -p /etc/librepages/nginx/sites-available
|
||||
- mkdir -p /etc/librepages/nginx/sites-enabled/
|
||||
- sed -i "s%include \/etc\/nginx\/sites-enabled%include \/etc\/librepages\/nginx\/sites-enabled%" /etc/nginx/nginx.conf
|
||||
- mkdir /var/www/website/ && echo "Hello Librepages" > /var/www/website/index.html
|
||||
# nginx_le_bind runs this command.
|
||||
# Testing beforehand to ensure it is setup properly
|
||||
- sudo nginx -t
|
||||
- make
|
||||
- make test
|
||||
- make release
|
||||
|
@ -21,15 +27,12 @@ pipeline:
|
|||
|
||||
publish:
|
||||
image: plugins/docker
|
||||
when:
|
||||
event: push
|
||||
branch: master
|
||||
settings:
|
||||
username: realaravinth
|
||||
password:
|
||||
from_secret: DOCKER_TOKEN
|
||||
repo: realaravinth/librepages-conductor
|
||||
tags: latest
|
||||
|
||||
#services:
|
||||
# database:
|
||||
# image: postgres
|
||||
# environment:
|
||||
# - POSTGRES_PASSWORD=password
|
||||
|
|
1
env/libconductor/src/event_types.rs
vendored
1
env/libconductor/src/event_types.rs
vendored
|
@ -30,6 +30,7 @@ pub enum EventType {
|
|||
},
|
||||
|
||||
Config {
|
||||
hostname: String,
|
||||
data: LibConfig,
|
||||
},
|
||||
}
|
||||
|
|
1
env/nginx_bind_le/.gitignore
vendored
Normal file
1
env/nginx_bind_le/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
target/
|
1118
env/nginx_bind_le/Cargo.lock
generated
vendored
Normal file
1118
env/nginx_bind_le/Cargo.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
24
env/nginx_bind_le/Cargo.toml
vendored
Normal file
24
env/nginx_bind_le/Cargo.toml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "nginx_bind_le"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
include = ["/templates"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features=["derive"]}
|
||||
serde_json = { version ="1", features = ["raw_value"]}
|
||||
async-trait = "0.1.57"
|
||||
tokio = { version = "1.23.0", features = ["process", "fs", "io-util"] }
|
||||
tera = "1.17.1"
|
||||
rust-embed = "6.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" }
|
||||
|
||||
[dependencies.libconductor]
|
||||
path = "../libconductor"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.23.0", features = ["rt-multi-thread", "macros", "rt"] }
|
102
env/nginx_bind_le/src/lib.rs
vendored
Normal file
102
env/nginx_bind_le/src/lib.rs
vendored
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use async_trait::async_trait;
|
||||
use tokio::process::Command;
|
||||
|
||||
use libconductor::*;
|
||||
|
||||
mod nginx;
|
||||
mod templates;
|
||||
|
||||
use nginx::Nginx;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NginxBindLEConductor;
|
||||
|
||||
const CONDUCTOR_NAME: &str = "NGINX_BIND_LE_CONDUCTOR";
|
||||
|
||||
#[async_trait]
|
||||
impl Conductor for NginxBindLEConductor {
|
||||
async fn process(&self, event: EventType) {
|
||||
match event {
|
||||
EventType::NewSite {
|
||||
hostname,
|
||||
path,
|
||||
branch: _branch,
|
||||
} => {
|
||||
Nginx::new_site(&hostname, &path, None).await.unwrap();
|
||||
}
|
||||
EventType::Config { hostname, data } => {
|
||||
unimplemented!();
|
||||
// Nginx::new_site(&hostname, &path, Some(data)).await.unwrap();
|
||||
}
|
||||
EventType::DeleteSite { hostname } => {
|
||||
Nginx::rm_site(&hostname).await.unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
CONDUCTOR_NAME
|
||||
}
|
||||
|
||||
async fn health(&self) -> bool {
|
||||
nginx::Nginx::env_exists() && nginx::Nginx::status().await
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::process::Stdio;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_good() {
|
||||
const HOSTNAME: &str = "lab.batsense.net";
|
||||
let c = NginxBindLEConductor {};
|
||||
assert_eq!(c.name(), CONDUCTOR_NAME);
|
||||
assert!(c.health().await);
|
||||
if Nginx::site_exists(HOSTNAME) {
|
||||
c.process(EventType::DeleteSite {
|
||||
hostname: HOSTNAME.into(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
c.process(EventType::NewSite {
|
||||
hostname: HOSTNAME.into(),
|
||||
branch: "librepages".into(),
|
||||
path: "/var/www/website/".into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
let out = tokio::process::Command::new("sudo")
|
||||
.arg("nginx")
|
||||
.arg("-T")
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let expected = format!("server_name {HOSTNAME}");
|
||||
let out = String::from_utf8(out.stdout).unwrap();
|
||||
assert!(out.contains(&expected));
|
||||
c.process(EventType::DeleteSite {
|
||||
hostname: HOSTNAME.into(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
132
env/nginx_bind_le/src/nginx.rs
vendored
Normal file
132
env/nginx_bind_le/src/nginx.rs
vendored
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tera::*;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::templates::*;
|
||||
|
||||
pub struct Nginx;
|
||||
|
||||
impl Nginx {
|
||||
pub async fn reload() -> MyResult<()> {
|
||||
Command::new("sudo")
|
||||
.arg("nginx")
|
||||
.arg("-s")
|
||||
.arg("reload")
|
||||
.spawn()?
|
||||
.wait()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn status() -> bool {
|
||||
async fn run_async_cmd(cmd: &mut Command) -> bool {
|
||||
if let Ok(mut child) = cmd.spawn() {
|
||||
if let Ok(res) = child.wait().await {
|
||||
return res.success();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
run_async_cmd(Command::new("sudo").arg("nginx").arg("-t")).await
|
||||
}
|
||||
|
||||
pub async fn new_site(
|
||||
hostname: &str,
|
||||
path: &str,
|
||||
config: Option<libconfig::Config>,
|
||||
) -> MyResult<()> {
|
||||
let config = CreateSite::new(hostname, path, config);
|
||||
let contents = config.render();
|
||||
let staging = Self::get_staging(hostname);
|
||||
let prod = Self::get_prod(hostname);
|
||||
|
||||
let mut file = fs::File::create(&staging).await?;
|
||||
file.write_all(contents.as_bytes()).await?;
|
||||
file.sync_all().await?;
|
||||
fs::symlink(&staging, &prod).await?;
|
||||
Self::reload().await
|
||||
}
|
||||
|
||||
fn get_staging(hostname: &str) -> PathBuf {
|
||||
Path::new(NGINX_STAGING_CONFIG_PATH).join(hostname)
|
||||
}
|
||||
fn get_prod(hostname: &str) -> PathBuf {
|
||||
Path::new(NGINX_PRODUCTION_CONFIG_PATH).join(hostname)
|
||||
}
|
||||
|
||||
pub fn site_exists(hostname: &str) -> bool {
|
||||
Self::get_prod(hostname).exists()
|
||||
}
|
||||
|
||||
pub async fn rm_site(hostname: &str) -> MyResult<()> {
|
||||
let staging = Self::get_staging(hostname);
|
||||
let prod = Self::get_prod(hostname);
|
||||
|
||||
fs::remove_file(&prod).await?;
|
||||
fs::remove_file(&staging).await?;
|
||||
Self::reload().await
|
||||
}
|
||||
|
||||
pub fn env_exists() -> bool {
|
||||
let prod = Path::new(NGINX_PRODUCTION_CONFIG_PATH);
|
||||
let staging = Path::new(NGINX_STAGING_CONFIG_PATH);
|
||||
prod.exists() && prod.is_dir() && staging.exists() && staging.is_dir()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateSite {
|
||||
ctx: RefCell<Context>,
|
||||
}
|
||||
|
||||
pub const CREATE_SITE: TemplateFile = TemplateFile::new("create_site", "nginx/create-site.j2");
|
||||
pub const CREATE_SITE_FRAGMENT: TemplateFile =
|
||||
TemplateFile::new("new_site_frag", "nginx/_new_site.fragement.j2");
|
||||
|
||||
pub const HOSTNAME_KEY: &str = "hostname";
|
||||
pub const DOMAINS_KEY: &str = "domains";
|
||||
pub const PATH_KEY: &str = "path";
|
||||
pub const REDIRECTS_KEY: &str = "redirects";
|
||||
|
||||
pub const NGINX_STAGING_CONFIG_PATH: &str = "/etc/librepages/nginx/sites-available/";
|
||||
pub const NGINX_PRODUCTION_CONFIG_PATH: &str = "/etc/librepages/nginx/sites-enabled/";
|
||||
|
||||
type MyResult<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
impl CreateSite {
|
||||
fn new(hostname: &str, path: &str, config: Option<libconfig::Config>) -> Self {
|
||||
let ctx = RefCell::new(context());
|
||||
ctx.borrow_mut().insert(HOSTNAME_KEY, hostname);
|
||||
ctx.borrow_mut().insert(PATH_KEY, path);
|
||||
if let Some(config) = config {
|
||||
ctx.borrow_mut().insert(REDIRECTS_KEY, &config.redirects);
|
||||
ctx.borrow_mut().insert(DOMAINS_KEY, &config.domains);
|
||||
}
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
fn render(&self) -> String {
|
||||
TEMPLATES
|
||||
.render(CREATE_SITE.name, &self.ctx.borrow())
|
||||
.unwrap()
|
||||
}
|
||||
}
|
73
env/nginx_bind_le/src/templates.rs
vendored
Normal file
73
env/nginx_bind_le/src/templates.rs
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use lazy_static::lazy_static;
|
||||
use rust_embed::RustEmbed;
|
||||
use tera::*;
|
||||
|
||||
pub const PAYLOAD_KEY: &str = "payload";
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TEMPLATES: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
|
||||
for template in [crate::nginx::CREATE_SITE, crate::nginx::CREATE_SITE_FRAGMENT].iter() {
|
||||
template.register(&mut tera).expect(template.name);
|
||||
}
|
||||
// tera.autoescape_on(vec![".html", ".sql"]);
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "templates/"]
|
||||
pub struct Templates;
|
||||
|
||||
impl Templates {
|
||||
pub fn get_template(t: &TemplateFile) -> Option<String> {
|
||||
match Self::get(t.path) {
|
||||
Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context() -> Context {
|
||||
let mut ctx = Context::new();
|
||||
ctx
|
||||
}
|
||||
|
||||
pub struct TemplateFile {
|
||||
pub name: &'static str,
|
||||
pub path: &'static str,
|
||||
}
|
||||
|
||||
impl TemplateFile {
|
||||
pub const fn new(name: &'static str, path: &'static str) -> Self {
|
||||
Self { name, path }
|
||||
}
|
||||
|
||||
pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
|
||||
t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
|
||||
use std::path::Path;
|
||||
t.add_template_file(Path::new("templates/").join(self.path), Some(self.name))
|
||||
}
|
||||
}
|
1
env/nginx_bind_le/templates/create_cert_le.sh.j2
vendored
Normal file
1
env/nginx_bind_le/templates/create_cert_le.sh.j2
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
sudo certbot --nginx -d {{ hostname }}
|
29
env/nginx_bind_le/templates/nginx/_new_site.fragement.j2
vendored
Normal file
29
env/nginx_bind_le/templates/nginx/_new_site.fragement.j2
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
server {
|
||||
# serve website on port 80
|
||||
listen [::]:80;
|
||||
listen 80;
|
||||
|
||||
# write error logs to file
|
||||
error_log /var/log/nginx/{{ hostname }}.error.log;
|
||||
# write access logs to file
|
||||
access_log /var/log/nginx/{{ hostname }}.access.log;
|
||||
|
||||
# serve only on this domain:
|
||||
server_name {{ hostname }};
|
||||
|
||||
# use files from this directory
|
||||
root {{ path }};
|
||||
|
||||
# remove .html from URL; it is cleaner this way
|
||||
rewrite ^(/.*)\.html(\?.*)?$ $1$2 permanent;
|
||||
|
||||
{% if redirects %}
|
||||
{% for redirect in redirects %}
|
||||
rewrite ^/{{redirect.from}}$ /{{ redirect.to }} redirect;
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
# when a request is received, try the index.html in the directory
|
||||
# or $uri.html
|
||||
try_files $uri/index.html $uri.html $uri/ $uri =404;
|
||||
}
|
7
env/nginx_bind_le/templates/nginx/create-site.j2
vendored
Normal file
7
env/nginx_bind_le/templates/nginx/create-site.j2
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% include "new_site_frag" %}
|
||||
|
||||
{% if domains %}
|
||||
{% for hostname in domains %}
|
||||
{% include "new_site_frag" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
Loading…
Reference in a new issue