Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F12241426
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
35 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/Cargo.toml b/Cargo.toml
index ca5b02e..c58477f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,28 +1,29 @@
[package]
name = "alkane"
-version = "0.2.0"
+version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+axum = "0.8.4"
env_logger = "^0.11.5"
lazy_static = "^1.5.0"
-limiting-factor = "^0.8.0"
+limiting-factor-axum = "0.1.0"
log = "^0.4.22"
-rocket = "^0.4.11"
-rocket_codegen = "^0.4.11"
serde_yaml = "^0.9.33"
[dependencies.clap]
version = "~4.5.17"
features = ["derive"]
-[dependencies.rocket_contrib]
-version = "^0.4.11"
-default-features = false
-features = ["json"]
-
[dependencies.serde]
version = "^1.0.210"
features = ["derive"]
+
+[dependencies.tokio]
+version = "1.47.1"
+features = [
+ "macros",
+ "rt-multi-thread",
+]
diff --git a/README.md b/README.md
index 968848f..d252428 100644
--- a/README.md
+++ b/README.md
@@ -1,153 +1,148 @@
# Alkane
## Features
The `alkane` command allows to manage the Nasqueron PaaS Alkane,
to host PHP sites through nginx and php-fpm, and static sites
directly through nginx.
It can work in two complementary modes:
- as a command, to allow local system administration task and debug
- as a server, to allow components to interact with it for automation
For example, to update the website foo.domain.tld, you can:
- use the command `alkane update foo.domain.tld`
- send a POST request to `http://localhost:10206/update/foo.domain.tld`
For example, you can use the server to allow Jenkins or your continuous
delivery system to trigger an update process.
Both modes will run the same action code.
## Usage
### Alkane update
The update feature allows to update a site according configured instructions:
- do something simple like `git pull`
- download a Jenkins artifact
- run a recipe like `git pull && composer update` or `make update`
- it can ask php-fpm to reload, so Opcache picks the new code
The instructions aren't given as command argument or HTTP request, but
preconfigured on the server.
Alkane is responsible to update the local site content, to help deploying
your artefact, but not to update nginx or certificates configuration.
On Nasqueron servers, nginx is provisioned through Salt.
When triggered from HTTP API by an external service like CD,
the raw POST content is available to the instructions script
under ALKANE_SITE_CONTEXT environment variable.
### Alkane init
The init feature works similarly than the update process, but is responsible
to deploy the site the first time, when it doesn't exist.
### Alkane misc commands
- **is-present**: determine if a site is hosted on the PaaS
- **deploy**: call `init` or `update` as needed
### Alkane server
To run the **Alkane** server and expose the API, use `alkane server`.
The following environment variables allow to configure the server:
| Variable | Description | Default value |
|--------------------|-----------------|---------------|
-| ROCKET_PORT | Server port | 8000 |
-| ROCKET_ADDRESS | Address to bind | 0.0.0.0 |
-
-The following options allow to configure the server:
-
-| Argument | Description | Default value |
-|--------------------|-----------------|---------------|
-| --mounting-point | Mounting point | / |
+| APP_PORT | Server port | 8000 |
+| APP_ADDRESS | Address to bind | 0.0.0.0 |
+| APP_MOUNT_POINT | Mounting point | / |
Nasqueron servers expose Alkane on the port 10206, for the alkane C2H6.
## Configuration
### Configuration file
The configuration is written in YAML and is seeked at:
- .alkane.conf
- /usr/local/etc/alkane.conf
- /etc/alkane.conf
This repository provides an example `.alkane.conf` both for reference
and for testing suite: path matches the test/data folder.
### Recipes scripts
Each site should have two scripts in /usr/local/libexec/alkane/<site name>,
or any other directory set as `roots.recipes` in the configuration:
- init: called by `alkane init`
- update: called by `alkane update`
Several environment variables are available to those scripts:
| Variable | Description |
|---------------------|--------------------------------------------|
| ALKANE_RECIPES_PATH | The *root* path to the recipes |
| ALKANE_SITE_NAME | The site name, for example the FQDN |
| ALKANE_SITE_PATH | The full path to the site content |
| ALKANE_SITE_CONTEXT | Arbitrary context sent to HTTP API, if set |
The following security consideration should be exercised:
- if you symlink or set the recipes root to a file in the site repository,
you trust the site to run arbitrary code as the alkane user
- don't trust blindly context information, it can be false or malformed,
and is a vector for attack if your CD is compromised
- if you run the server HTTP API, listen only to private IP address,
or use a firewall to block the port, it shouldn't be reachable publicly
## Development notes
### Design goals
Alkane update process helps us to separate concerns:
- Infrastructure as code configure the server and provisions Alkane config
- Both Salt and Jenkins can when needed request a site update
- CD prepare the build (npm build for example) and ask Alkane to deploy it
At Nasqueron, we wanted to get a more standardized way to work and ensure
the site ends in the same state if deployed through Salt or Jenkins CD.
Currently, Alkane isn't responsible to rollback the deployment. To rollback,
you can revert the commit, and trigger CD for it and Alkane will pick it
like usual.
Alkane can help you to run Canary tests. To do so, you can run `alkane update`
on a server, or a small range of servers, observe traffic, and take the
decision to deploy everywhere from responses from those.
### License
(c) 2023, Sébastien Santoro, Nasqueron, some rights reserved.
Released under BSD-2-Clause license. See [license](LICENSE).
### Contribute
Alkane is written in Rust using:
- - Rocket and Limiting Factor for the HTTP API
+ - Axum for the HTTP API
- Clap to parse arguments
- serde_yaml to deserialize configuration files
Run `make` to build the first time: it will download public suffix list
and then run `cargo build`. Afterwards, you can directly use cargo.
Issues can be tracked at https://devcentral.nasqueron.org/
To send your change, you can follow this guide:
https://agora.nasqueron.org/How_to_contribute_code
diff --git a/src/actions.rs b/src/actions.rs
index e23d046..43dca51 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -1,109 +1,108 @@
// -------------------------------------------------------------
// Alkane :: Actions
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// -------------------------------------------------------------
-use crate::command::ServerArgs;
use crate::config::AlkaneConfig;
use crate::db::Database;
use crate::deploy::AlkaneDeployError;
use crate::deploy::DeployError;
use crate::runner::store::RecipesStore;
use crate::runner::RecipeStatus;
use crate::server::kernel::run;
// -------------------------------------------------------------
// Actions only available in CLI
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-pub fn serve(args: ServerArgs, config: AlkaneConfig) {
- run(config, &args.mounting_point);
+pub async fn serve(config: AlkaneConfig) -> bool {
+ run(config).await
}
// -------------------------------------------------------------
// Actions available both for CLI and HTTP
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
fn run_deployment_action(
site_name: &str,
context: Option<String>,
config: &AlkaneConfig,
action: &str,
) -> Result<RecipeStatus, DeployError> {
let db = Database::from_config(config).ok_or_else(|| {
let error = AlkaneDeployError::new("Can't initialize database", site_name, action);
DeployError::Alkane(error)
})?;
let recipes = RecipesStore::from_config(config).ok_or_else(|| {
let error = AlkaneDeployError::new("Can't initialize recipes store", site_name, action);
DeployError::Alkane(error)
})?;
let site = config.get_site(site_name, context).ok_or_else(|| {
let error = AlkaneDeployError::new("Can't resolve site path", site_name, action);
DeployError::Alkane(error)
})?;
let status = recipes.run_recipe(&site, action);
if action == "init" && status == RecipeStatus::Success {
db.set_initialized(&site.name);
}
Ok(status)
}
pub fn initialize(
site_name: &str,
context: Option<String>,
config: &AlkaneConfig,
) -> Result<RecipeStatus, DeployError> {
run_deployment_action(site_name, context, config, "init")
}
pub fn update(
site_name: &str,
context: Option<String>,
config: &AlkaneConfig,
) -> Result<RecipeStatus, DeployError> {
run_deployment_action(site_name, context, config, "update")
}
pub fn deploy(
site_name: &str,
context: Option<String>,
config: &AlkaneConfig,
) -> Result<RecipeStatus, DeployError> {
if is_present(site_name, config) {
run_deployment_action(site_name, context, config, "update")
} else {
run_deployment_action(site_name, context, config, "init")
}
}
pub fn is_present(site_name: &str, config: &AlkaneConfig) -> bool {
match Database::from_config(&config) {
None => false,
Some(db) => db.is_initialized(site_name),
}
}
// -------------------------------------------------------------
// Tests
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn test_is_present() {
let config = AlkaneConfig::load().unwrap();
assert_eq!(true, is_present("foo.acme.tld", &config));
assert_eq!(false, is_present("notexisting.acme.tld", &config));
}
}
diff --git a/src/command.rs b/src/command.rs
index cef8ed6..3cc2544 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -1,94 +1,88 @@
// -------------------------------------------------------------
// Alkane :: Commands
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// -------------------------------------------------------------
use clap::{Args, Parser};
// -------------------------------------------------------------
// Main command
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#[derive(Debug, Parser)]
#[command(name = "alkane")]
#[clap(author="Nasqueron project", version, about="Manage Alkane PaaS", long_about=None)]
pub enum AlkaneCommand {
/// Launch an HTTP server to expose the Alkane REST API
- Server(ServerArgs),
+ Server,
/// Initialize a site
#[command(arg_required_else_help = true)]
Init(DeployArgs),
/// Update site assets to latest version
#[command(arg_required_else_help = true)]
Update(DeployArgs),
/// Initialize of if already initialized update a site
#[command(arg_required_else_help = true)]
Deploy(DeployArgs),
/// Determine if a domain is served on our PaaS
#[command(name = "is-present", arg_required_else_help = true)]
IsPresent(IsPresentArgs),
}
// -------------------------------------------------------------
// Subcommands arguments
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-#[derive(Debug, Args)]
-pub struct ServerArgs {
- #[arg(long, default_value = "/")]
- pub mounting_point: String,
-}
-
#[derive(Debug, Args)]
pub struct DeployArgs {
/// The name of the site to deploy, using sub.domain.tld format
pub site_name: String,
/// The artifact to deploy. Allows CD to give metadata or an URL to download last artifact
pub artifact: Option<String>,
}
#[derive(Debug, Args)]
pub struct IsPresentArgs {
#[arg(short, long, default_value_t = false)]
pub quiet: bool,
/// The name of the site to deploy, using sub.domain.tld format
pub site_name: String,
}
// -------------------------------------------------------------
// Helper methods
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
pub trait ToStatusCode {
fn to_status_code(&self) -> i32;
}
impl ToStatusCode for bool {
fn to_status_code(&self) -> i32 {
if *self {
0
} else {
1
}
}
}
impl<T> ToStatusCode for Option<T> {
fn to_status_code(&self) -> i32 {
self.is_some().to_status_code()
}
}
impl<T, E> ToStatusCode for Result<T, E> {
fn to_status_code(&self) -> i32 {
self.is_ok().to_status_code()
}
}
diff --git a/src/config.rs b/src/config.rs
index 0381cee..8080c66 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,200 +1,200 @@
// -------------------------------------------------------------
// Alkane :: Configuration
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// -------------------------------------------------------------
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, MAIN_SEPARATOR_STR};
use lazy_static::lazy_static;
use log::info;
use serde::Deserialize;
use serde_yaml;
use crate::runner::site::Site;
use crate::services::tld::extract_domain_parts;
// -------------------------------------------------------------
// Constants:
// - ROOTS: default path for root directories
// - CONFIG_PATHS: paths where to find the configuration file
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
lazy_static! {
static ref ROOTS: HashMap<&'static str, &'static str> = {
let mut map = HashMap::new();
map.insert("db", "/var/db/alkane");
map.insert("sites", "/var/wwwroot");
map.insert("recipes", "/usr/local/libexec/alkane");
map
};
static ref CONFIG_PATHS: Vec<&'static str> = vec![
".alkane.conf",
"/etc/alkane.conf",
"/usr/local/etc/alkane.conf",
];
}
// -------------------------------------------------------------
// AlkaneConfig is the deserialized representation of
// the Alkane configuration file alkane.conf
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-#[derive(Debug, Deserialize)]
+#[derive(Clone, Debug, Deserialize)]
pub struct AlkaneConfig {
/// The paths to the root directories used by Alkane
roots: HashMap<String, String>,
/// The template for a site directory
site_directory_template: String,
}
#[derive(Debug)]
pub enum AlkaneConfigError {
IO(std::io::Error),
YAML(serde_yaml::Error),
FileNotFound,
}
impl AlkaneConfig {
pub fn load() -> Result<Self, AlkaneConfigError> {
match Self::find() {
None => Err(AlkaneConfigError::FileNotFound),
Some(path) => {
info!("Configuration file found: {}", &path);
let file = File::open(&path).map_err(AlkaneConfigError::IO)?;
serde_yaml::from_reader(file).map_err(AlkaneConfigError::YAML)
}
}
}
fn find() -> Option<String> {
CONFIG_PATHS
.iter()
.filter(|&path| Path::new(path).exists())
.map(|&path| String::from(path))
.next()
}
pub fn get_root(&self, key: &str) -> Option<String> {
if self.roots.contains_key(key) {
self.roots.get(key).map(|s| String::from(s))
} else {
ROOTS.get(key).map(|s| String::from(*s))
}
}
pub fn get_site(&self, site_name: &str, context: Option<String>) -> Option<Site> {
self.get_site_path(site_name).map(|path| Site {
name: site_name.to_string(),
context,
path,
})
}
pub fn get_site_path(&self, site_name: &str) -> Option<String> {
let root = self.get_root("sites")?;
let root = root.replace("/", MAIN_SEPARATOR_STR);
let subdir = self.resolve_site_subdir(site_name)?;
Path::new(&root)
.join(&subdir)
.to_str()
.map(|path| String::from(path))
}
fn resolve_site_subdir(&self, site_name: &str) -> Option<String> {
let subdir = self
.site_directory_template
.clone()
.replace("/", MAIN_SEPARATOR_STR)
.replace("%fqdn%", site_name);
if contains_domain_parts_variables(&subdir) {
extract_domain_parts(site_name).map(|parts| {
subdir
.replace("%subdomain%", &parts.0)
.replace("%domain%", &parts.1)
.replace("%tld%", &parts.2)
})
} else {
Some(subdir)
}
}
}
// -------------------------------------------------------------
// Helper methods to extract domain name parts
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
fn contains_domain_parts_variables<S>(site_name: S) -> bool
where
S: AsRef<str>,
{
let site_name = site_name.as_ref();
site_name.contains("%subdomain%")
|| site_name.contains("%domain%")
|| site_name.contains("%tld%")
}
// -------------------------------------------------------------
// Tests
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn test_load() {
let config = AlkaneConfig::load();
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(
&config.site_directory_template,
"%domain%.%tld%/%subdomain%"
);
}
#[test]
pub fn test_root() {
let config = AlkaneConfig::load().unwrap();
assert_eq!(None, config.get_root("notexisting"));
assert_eq!(Some(String::from("tests/data/db")), config.get_root("db"));
}
#[test]
pub fn test_get_site_path() {
let config = AlkaneConfig::load().unwrap();
let expected = Path::new("tests")
.join("data")
.join("wwwroot")
.join("example.org")
.join("foo");
let expected = String::from(expected.to_str().unwrap());
let expected = Some(expected);
assert_eq!(expected, config.get_site_path("foo.example.org"));
}
#[test]
pub fn test_contains_domain_parts_variables() {
assert_eq!(
true,
contains_domain_parts_variables("%domain%/%subdomain%")
);
assert_eq!(false, contains_domain_parts_variables("%fqdn%"));
assert_eq!(false, contains_domain_parts_variables(""));
}
}
diff --git a/src/main.rs b/src/main.rs
index a7fd581..57253c6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,96 +1,97 @@
// -------------------------------------------------------------
// Alkane
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// Description: Manage nginx and php-fpm Alkane PaaS
// -------------------------------------------------------------
-#![feature(decl_macro)]
-
use std::process::exit;
use clap::Parser;
use crate::actions::*;
use crate::command::{AlkaneCommand, ToStatusCode};
use crate::config::AlkaneConfig;
use crate::deploy::DeployError;
use crate::runner::RecipeStatus;
// -------------------------------------------------------------
// Modules
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
mod actions;
mod command;
mod config;
mod db;
mod deploy;
mod runner;
mod server;
mod services;
// -------------------------------------------------------------
// Application entry point
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-fn main() {
+#[tokio::main]
+async fn main() {
env_logger::init();
let command = AlkaneCommand::parse(); // Will exit if argument is missing or --help/--version provided.
let config = match AlkaneConfig::load() {
Ok(config) => config,
Err(error) => {
eprintln!("Can't load configuration: {:?}", error);
exit(4);
}
};
match command {
- AlkaneCommand::Server(args) => {
- serve(args, config);
+ AlkaneCommand::Server => {
+ let result = serve(config).await;
+
+ exit(result.to_status_code())
}
AlkaneCommand::Update(args) => {
let result = update(&args.site_name, None, &config);
deploy_exit(result);
}
AlkaneCommand::Init(args) => {
let result = initialize(&args.site_name, None, &config);
deploy_exit(result);
}
AlkaneCommand::Deploy(args) => {
let result = deploy(&args.site_name, None, &config);
deploy_exit(result);
}
AlkaneCommand::IsPresent(args) => {
let is_present = is_present(&args.site_name, &config);
if !args.quiet {
if is_present {
let path = config.get_site_path(&args.site_name).unwrap();
println!("{}", path);
} else {
eprintln!("Site is absent.")
}
}
exit(is_present.to_status_code());
}
}
}
fn deploy_exit(result: Result<RecipeStatus, DeployError>) {
match result {
Ok(status) => exit(status.to_status_code()),
Err(error) => {
eprintln!("{}", error);
exit(16);
}
}
}
diff --git a/src/runner/store.rs b/src/runner/store.rs
index 54f78fc..15aa7ee 100644
--- a/src/runner/store.rs
+++ b/src/runner/store.rs
@@ -1,123 +1,125 @@
// -------------------------------------------------------------
// Alkane :: Runner :: Recipes store
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// -------------------------------------------------------------
use std::collections::HashMap;
use std::path::Path;
use crate::config::AlkaneConfig;
use crate::runner::run;
use crate::runner::site::Site;
use crate::runner::RecipeStatus;
pub struct RecipesStore {
root: String,
}
impl RecipesStore {
pub fn new<S>(root: S) -> Self
where
S: AsRef<str>,
{
Self {
root: root.as_ref().to_string(),
}
}
pub fn from_config(config: &AlkaneConfig) -> Option<Self> {
config.get_root("recipes").map(Self::new)
}
fn get_recipe_path(&self, site_name: &str, action: &str) -> String {
Path::new(&self.root)
.join(site_name)
.join(action)
.to_str()
.expect("Can't read recipe path as UTF-8")
.to_string()
}
pub fn run_recipe(&self, site: &Site, action: &str) -> RecipeStatus {
let command = self.get_recipe_path(&site.name, action);
let environment = self.get_environment(&site);
run(command, Vec::new(), environment)
}
fn get_environment(&self, site: &Site) -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("ALKANE_RECIPES_PATH".to_string(), self.root.clone());
map.insert("ALKANE_SITE_NAME".to_string(), site.name.clone());
map.insert("ALKANE_SITE_PATH".to_string(), site.path.clone());
if let Some(context) = &site.context {
- map.insert("ALKANE_SITE_CONTEXT".to_string(), context.clone());
+ if !context.is_empty() {
+ map.insert("ALKANE_SITE_CONTEXT".to_string(), context.clone());
+ }
}
map
}
}
// -------------------------------------------------------------
// Tests
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#[cfg(test)]
mod tests {
use std::path::MAIN_SEPARATOR_STR;
use super::*;
#[test]
pub fn test_get_recipe_path() {
let expected = "tests/data/recipes/foo.acme.tld/update".replace("/", MAIN_SEPARATOR_STR);
let test_store_path = "tests/data/recipes".replace("/", MAIN_SEPARATOR_STR);
let store = RecipesStore::new(&test_store_path);
assert_eq!(expected, store.get_recipe_path("foo.acme.tld", "update"));
}
#[test]
pub fn test_get_environment() {
let mut expected = HashMap::new();
expected.insert(
"ALKANE_RECIPES_PATH".to_string(),
"tests/data/recipes".replace("/", MAIN_SEPARATOR_STR),
);
expected.insert("ALKANE_SITE_NAME".to_string(), "foo.acme.tld".to_string());
expected.insert(
"ALKANE_SITE_PATH".to_string(),
"tests/data/wwwroot/acme.tld/foo".replace("/", MAIN_SEPARATOR_STR),
);
let test_store_path = "tests/data/recipes".replace("/", MAIN_SEPARATOR_STR);
let store = RecipesStore::new(&test_store_path);
let site = Site {
name: "foo.acme.tld".to_string(),
path: "tests/data/wwwroot/acme.tld/foo".replace("/", MAIN_SEPARATOR_STR),
context: None,
};
assert_eq!(expected, store.get_environment(&site));
}
#[test]
pub fn test_get_environment_with_context() {
let site = Site {
name: "foo.acme.tld".to_string(),
path: "/path/to/site".to_string(),
context: Some("CH3-CH3".to_string()),
};
let environment = RecipesStore::new("/path/to/recipes").get_environment(&site);
assert!(environment.contains_key("ALKANE_SITE_CONTEXT"));
assert_eq!("CH3-CH3", environment["ALKANE_SITE_CONTEXT"])
}
}
diff --git a/src/server/kernel.rs b/src/server/kernel.rs
index 7c4861c..3fa451c 100644
--- a/src/server/kernel.rs
+++ b/src/server/kernel.rs
@@ -1,33 +1,47 @@
// -------------------------------------------------------------
// Alkane :: Server :: Kernel
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// -------------------------------------------------------------
-use rocket::ignite;
-use rocket_codegen::routes;
+use axum::Router;
+use axum::routing::{get, post};
+use limiting_factor_axum::app::{App, ServerConfig};
use crate::config::AlkaneConfig;
use crate::server::requests::*;
// -------------------------------------------------------------
// Server entry point
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-pub fn run(config: AlkaneConfig, mounting_point: &str) {
- let routes = routes![
+pub fn get_default_config () -> ServerConfig {
+ ServerConfig {
+ address: "0.0.0.0".to_string(),
+ port: 8000,
+ mount_point: "/".to_string(),
+ }
+}
+
+pub fn get_router () -> Router<AlkaneConfig> {
+ Router::new()
+
// Monitoring
- status,
+ .route("/status", get(status))
+
// Alkane API
- init,
- update,
- deploy,
- is_present,
- ];
-
- ignite()
- .manage(config)
- .mount(mounting_point, routes)
- .launch();
+ .route("/init/{site_name}", post(init))
+ .route("/update/{site_name}", post(update))
+ .route("/deploy/{site_name}", post(deploy))
+ .route("/is_present/{site_name}", get(is_present))
+}
+
+pub async fn run(alkane_config: AlkaneConfig) -> bool {
+ let server_config = ServerConfig::from_env_or(get_default_config());
+
+ let router = get_router()
+ .with_state(alkane_config);
+
+ App::new(server_config, router).run().await
}
diff --git a/src/server/requests.rs b/src/server/requests.rs
index ee3dd6a..d49352b 100644
--- a/src/server/requests.rs
+++ b/src/server/requests.rs
@@ -1,97 +1,96 @@
// -------------------------------------------------------------
// Alkane :: Server :: Requests
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Project: Nasqueron
// License: BSD-2-Clause
// -------------------------------------------------------------
-use limiting_factor::api::guards::RequestBody;
-use limiting_factor::api::replies::{ApiJsonResponse, ApiResponse};
-use log::{debug, info, warn};
-use rocket::State;
-use rocket_codegen::{get, post};
+use axum::extract::{Path, State};
+use axum::http::StatusCode;
+
+use limiting_factor_axum::api::guards::AxumRequestBody as RequestBody;
+use limiting_factor_axum::api::replies::{ApiJsonResponse, ApiResponse, FailureResponse};
+use log::{debug, info, warn};
use crate::actions;
use crate::config::AlkaneConfig;
+use crate::deploy::DeployError;
use crate::runner::RecipeStatus;
// -------------------------------------------------------------
// Monitoring
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-#[get("/status")]
-pub fn status() -> &'static str {
+pub async fn status() -> &'static str {
"ALIVE"
}
// -------------------------------------------------------------
// Alkane requests
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-#[get("/is_present/<site_name>")]
-pub fn is_present(site_name: String, config: State<AlkaneConfig>) -> ApiJsonResponse<bool> {
+pub async fn is_present(
+ Path(site_name): Path<String>,
+ State(config): State<AlkaneConfig>,
+) -> ApiJsonResponse<bool> {
actions::is_present(&site_name, &config).into_json_response()
}
-#[post("/init/<site_name>", data = "<context>")]
-pub fn init(
- site_name: String,
+pub async fn init(
+ Path(site_name): Path<String>,
+ State(config): State<AlkaneConfig>,
context: RequestBody,
- config: State<AlkaneConfig>,
) -> ApiJsonResponse<RecipeStatus> {
info!("Deploying {}", &site_name);
let context = context.into_optional_string();
debug!("Context: {:?}", &context);
- match actions::initialize(&site_name, context, &config) {
- Ok(status) => status.into_json_response(),
- Err(error) => {
- warn!("{}", error);
-
- RecipeStatus::Error.into_json_response()
- }
- }
+ actions::initialize(&site_name, context, &config)
+ .into_json_response()
}
-#[post("/update/<site_name>", data = "<context>")]
-pub fn update(
- site_name: String,
+pub async fn update(
+ Path(site_name): Path<String>,
+ State(config): State<AlkaneConfig>,
context: RequestBody,
- config: State<AlkaneConfig>,
) -> ApiJsonResponse<RecipeStatus> {
info!("Deploying {}", &site_name);
let context = context.into_optional_string();
debug!("Context: {:?}", &context);
- match actions::update(&site_name, context, &config) {
- Ok(status) => status.into_json_response(),
- Err(error) => {
- warn!("{}", error);
-
- RecipeStatus::Error.into_json_response()
- }
- }
+ actions::update(&site_name, context, &config)
+ .into_json_response()
}
-#[post("/deploy/<site_name>", data = "<context>")]
-pub fn deploy(
- site_name: String,
+pub async fn deploy(
+ Path(site_name): Path<String>,
+ State(config): State<AlkaneConfig>,
context: RequestBody,
- config: State<AlkaneConfig>,
) -> ApiJsonResponse<RecipeStatus> {
info!("Deploying {}", &site_name);
let context = context.into_optional_string();
debug!("Context: {:?}", &context);
- match actions::deploy(&site_name, context, &config) {
- Ok(status) => status.into_json_response(),
- Err(error) => {
- warn!("{}", error);
+ actions::deploy(&site_name, context, &config)
+ .into_json_response()
+}
+
+// -------------------------------------------------------------
+// Custom error handling
+//
+// Deploy errors are returned as 400 + the Alkane error message
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+impl FailureResponse for DeployError {
+ fn status_code(&self) -> StatusCode {
+ StatusCode::BAD_REQUEST
+ }
- RecipeStatus::Error.into_json_response()
- }
+ fn response(&self) -> String {
+ warn!("{}", self); // Server log
+ format!("{}", self) // API response
}
}
diff --git a/support/freebsd/rc.d/alkane b/support/freebsd/rc.d/alkane
index 460c4d9..922e834 100644
--- a/support/freebsd/rc.d/alkane
+++ b/support/freebsd/rc.d/alkane
@@ -1,101 +1,101 @@
#!/bin/sh
# PROVIDE: alkane
# REQUIRE: DAEMON
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# alkane_enable (bool): Set it to YES to enable alkane.
# Default is "NO".
# alkane_user (user): Set user to run alkane.
# Default is "deploy".
# alkane_dir_db (path): The database directory to use. Must match config.
# Default is "/var/db/alkane".
# alkane log_enable (bool): Enable logging to a specific log file
# Default is "YES"
# alkane_log_path (path): The path to the software log
# Default is "/var/log/alkane.log".
# alkane_log_level (str): The level of verbosity of the log (debug/info will output Rocket/Hyper too)
# Default is "warn"
# alkane_syslog_output_enable (bool): Set to enable syslog output.
# Default is "NO". See daemon(8).
# alkane_syslog_output_priority (str): Set syslog priority if syslog enabled.
# Default is "warn". See daemon(8).
# alkane_syslog_output_facility (str): Set syslog facility if syslog enabled.
# Default is "daemon". See daemon(8).
. /etc/rc.subr
name=alkane
rcvar=alkane_enable
load_rc_config $name
: ${alkane_enable:="NO"}
: ${alkane_user:="deploy"}
: ${alkane_dir_db:="/var/db/alkane"}
: ${alkane_log_enable:="YES"}
: ${alkane_log_path:="/var/log/alkane.log"}
: ${alkane_log_level:="warn"}
: ${alkane_config:="/usr/local/etc/alkane.conf"}
: ${alkane_address:="localhost"}
: ${alkane_port:="10206"}
DAEMON=$(/usr/sbin/daemon 2>&1 | grep -q syslog ; echo $?)
if [ ${DAEMON} -eq 0 ]; then
: ${alkane_syslog_output_enable:="NO"}
: ${alkane_syslog_output_priority:="warn"}
: ${alkane_syslog_output_facility:="daemon"}
if checkyesno alkane_syslog_output_enable; then
alkane_syslog_output_flags="-T ${name}"
if [ -n "${alkane_syslog_output_priority}" ]; then
alkane_syslog_output_flags="${alkane_syslog_output_flags} -s ${alkane_syslog_output_priority}"
fi
if [ -n "${alkane_syslog_output_facility}" ]; then
alkane_syslog_output_flags="${alkane_syslog_output_flags} -l ${alkane_syslog_output_facility}"
fi
fi
else
alkane_syslog_output_enable="NO"
alkane_syslog_output_flags=""
fi
if checkyesno alkane_log_enable; then
alkane_log_enable="YES"
alkane_log_flags="-o ${alkane_log_path}"
alkane_env="${alkane_env} RUST_LOG=warn,alkane=${alkane_log_level} ${alkane_env}"
else
alkane_log_enable="NO"
fi
-alkane_env="${alkane_env} ROCKET_SECRET=$(openssl rand -base64 32) ROCKET_PORT=${alkane_port} ROCKET_ADDRESS=${alkane_address} ${alkane_env}"
+alkane_env="${alkane_env} APP_PORT=${alkane_port} APP_ADDRESS=${alkane_address} ${alkane_env}"
pidfile=/var/run/alkane.pid
procname="/usr/local/bin/alkane"
command="/usr/sbin/daemon"
command_args="-f -t ${name} ${alkane_log_flags} ${alkane_syslog_output_flags} -p ${pidfile} ${procname} server"
start_precmd=alkane_startprecmd
required_files="$alkane_config"
alkane_startprecmd()
{
touch ${pidfile}
chown ${alkane_user} ${pidfile}
if [ "${alkane_log_enable}" = "YES" ]; then
touch ${alkane_log_path}
chown ${alkane_user} ${alkane_log_path}
fi
if [ ! -d ${alkane_dir_db} ]; then
install -d -o ${alkane_user} ${alkane_dir_db}
fi
}
run_rc_command "$1"
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Oct 12, 05:05 (23 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3062330
Default Alt Text
(35 KB)
Attached To
Mode
rALK Alkane
Attached
Detach File
Event Timeline
Log In to Comment