Compare commits

...

9 Commits

Author SHA1 Message Date
Aravinth Manivannan ca6bae8463
fix: CI: uses '%' instead of '/' in sed
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-12 21:59:31 +05:30
Aravinth Manivannan ad0c7ae94d
feat: CI: setup nginx to load custom configuration files and create dummy website for testing 2022-12-12 21:59:31 +05:30
Aravinth Manivannan e7068cfc7c
feat: test if vhost is loaded into nginx 2022-12-12 21:59:30 +05:30
Aravinth Manivannan 62278abede
feat: create and delete sites on nginx with tests 2022-12-12 21:59:30 +05:30
Aravinth Manivannan 1e5a3d57b5
feat: create site templates 2022-12-12 21:59:30 +05:30
Aravinth Manivannan e9237d3586
feat: include hostname while sending config event
hostname is autogenerated by librepages, config files, location in file
system, etc. are identified by hostname. So it makes sense to send
hostname, along with the custom domain names that the customer provides
2022-12-12 21:59:30 +05:30
Aravinth Manivannan efdff0bc26
fix: publish docker images only on pushes to master branch 2022-12-12 21:59:30 +05:30
Aravinth Manivannan 2401d40047
feat: CI: install nginx to run nginx conductor integration tests 2022-12-12 21:59:30 +05:30
Aravinth Manivannan bfaf077c02
feat: implement LibConductor::health for nginx conductor 2022-12-12 21:59:30 +05:30
11 changed files with 1500 additions and 9 deletions

View File

@ -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

View File

@ -30,6 +30,7 @@ pub enum EventType {
},
Config {
hostname: String,
data: LibConfig,
},
}

1
env/nginx_bind_le/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

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
View 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
View 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
View 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
View 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))
}
}

View File

@ -0,0 +1 @@
sudo certbot --nginx -d {{ hostname }}

View 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;
}

View File

@ -0,0 +1,7 @@
{% include "new_site_frag" %}
{% if domains %}
{% for hostname in domains %}
{% include "new_site_frag" %}
{% endfor %}
{% endif %}