Compare commits

...

23 commits

Author SHA1 Message Date
Aravinth Manivannan b0d94f91dc
feat: run conductor as root
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-30 05:18:43 +05:30
Aravinth Manivannan d40e8642de
feat: publish systemd service file
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-30 04:44:39 +05:30
Aravinth Manivannan 5851b686b4
feat: read config from /etc/librepages/conductor/ 2022-12-30 04:44:30 +05:30
Aravinth Manivannan db9115b90b
feat: read token from env var
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-29 18:34:39 +05:30
Aravinth Manivannan b15c72ef30
feat: read token from env var
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-29 17:48:30 +05:30
Aravinth Manivannan cd0589fb2e
feat: replace http auth with bearer auth
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-29 17:29:07 +05:30
Aravinth Manivannan 58eef6b3fa
feat: add prometheus instrumentation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-25 13:14:00 +05:30
Aravinth Manivannan 158ec03aab
fix: CI: dl.librepages.org username and bin name
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
2022-12-12 21:25:55 +05:30
Aravinth Manivannan 0e388d4e1e
fix: CI: apt install cmd
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-12 21:08:38 +05:30
Aravinth Manivannan ef0175eca0
fix: build and publish bins
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-12 21:00:18 +05:30
Aravinth Manivannan 838cb9387a
fix: CI: rm nginx workflows
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-12 20:31:55 +05:30
Aravinth Manivannan 15a17a184d
feat: publish bins to dl.librepages.org
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-12 20:27:32 +05:30
Aravinth Manivannan 96c1b807a7
feat: run tests on all workspaces and lints
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-12 15:15:11 +05:30
Aravinth Manivannan 4db76a0705
fix: launch on localhost 2022-12-12 15:14:55 +05:30
Aravinth Manivannan ccb9f0f046
fix: dummy conductor test is async 2022-12-12 15:14:41 +05:30
Aravinth Manivannan 3a2e6355da
feat: serve openapi docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-11 23:43:13 +05:30
Aravinth Manivannan a38411abaa
feat: openapi spec 2022-12-11 23:42:46 +05:30
Aravinth Manivannan b8246f0fd8
feat: restructure events schema for cleaner serialization
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-11 20:25:42 +05:30
Aravinth Manivannan 1fa28ef9b7
fix: use only one API key per conductor deployment
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-11 19:30:36 +05:30
Aravinth Manivannan a4f4903120
chore: lints and update config 11.x -> 13.x
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-11 19:27:59 +05:30
Aravinth Manivannan 1b40c44854
feat: delete stie event
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-11 18:49:04 +05:30
Aravinth Manivannan 20aa88ca51
fix: new site deployment requires extra info
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-09 18:04:26 +05:30
Aravinth Manivannan 8d9bc95bf6
feat: add event type to transmit deployment configuration 2022-12-09 18:02:48 +05:30
39 changed files with 1324 additions and 308 deletions

View file

@ -7,6 +7,17 @@ pipeline:
# - make migrate
- make
- make test
- make release
publish_bins:
image: rust
commands:
- apt update
- apt-get -y --no-install-recommends install gpg tar curl wget
- echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
- ./scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
secrets: [ RELEASE_BOT_GPG_SIGNING_KEY, DUMBSERVE_PASSWORD, GPG_PASSWORD ]
publish:
image: plugins/docker

689
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,19 +12,23 @@ build = "build.rs"
[dependencies]
actix-web = "4"
actix-web-prom = "0.6.0"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
lazy_static = "1.4.0"
log = "0.4.17"
pretty_env_logger = "0.4.0"
serde = { version = "1", features=["derive"]}
actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" }
libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" }
derive_builder = "0.11.2"
config = "0.11"
config = "0.13"
derive_more = "0.99.17"
url = { version = "2.2.2", features = ["serde"]}
serde_json = { version ="1", features = ["raw_value"]}
clap = { vesrion = "3.2.20", features = ["derive"]}
actix-web-httpauth = "0.8.0"
mime_guess = "2.0.4"
rust-embed = "6.4.2"
[dependencies.libconductor]
path = "./env/libconductor"

View file

@ -1,3 +1,12 @@
define lint
cargo fmt -v --all -- --emit files
cargo clippy --workspace --tests --all-features
endef
define test
cargo test --no-fail-fast --workspace --tests --all-features
endef
default: ## Build app in debug mode
cargo build
@ -25,8 +34,9 @@ env: ## Setup development environtment
cargo fetch
lint: ## Lint codebase
cargo fmt -v --all -- --emit files
cargo clippy --workspace --tests --all-features
$(call lint)
cd env/dummy_conductor && $(call lint)
cd env/libconductor && $(call lint)
#migrate: ## run migrations
# unset DATABASE_URL && cargo build
@ -36,7 +46,7 @@ release: ## Build app with release optimizations
cargo build --release
run: ## Run app in debug mode
cargo run
cargo run -- serve
#sqlx-offline-data: ## prepare sqlx offline data
@ -45,7 +55,8 @@ run: ## Run app in debug mode
# --all-features
test: ## Run all available tests
cargo test --no-fail-fast --workspace
$(call test)
cd env/dummy_conductor && $(call test)
xml-test-coverage: ## Generate code coverage report in XML format
cargo tarpaulin -t 1200 --out Xml

View file

@ -1,10 +1,9 @@
debug = true
source_code = "https://git.batsense.net/librepages/conductor"
conductor = "dummy"
api_keys = [
# CHANGE THIS!!
{ username = "librepages_api", password="longrandomlygeneratedpassword"}
]
[creds]
token="longrandomlygeneratedpassword"
[server]
# Please set a unique value, your mCaptcha instance's security depends on this being
@ -14,7 +13,7 @@ api_keys = [
port = 7000
#IP address. Enter 0.0.0.0 to listen on all available addresses
#ip= "0.0.0.0"
ip= "192.168.0.104"
ip= "127.0.0.1"
# enter your hostname, eg: example.com
domain = "localhost"
# Set true if you have setup TLS with a reverse proxy like Nginx.

View file

@ -0,0 +1,24 @@
[Unit]
Description=LibrePages Conductor: Easiest way to deploy websites. Conductor component
[Service]
Type=simple
User=root
ExecStart=/usr/bin/conductor serve
Restart=on-failure
RestartSec=1
SuccessExitStatus=3 4
RestartForceExitStatus=3 4
SystemCallArchitectures=native
MemoryDenyWriteExecute=true
NoNewPrivileges=true
Environment="RUST_LOG=info"
[Unit]
Wants=network-online.target
Wants=network-online.target
Requires=postgresql.service
After=syslog.target
[Install]
WantedBy=multi-user.target

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

16
docs/openapi/index.css Normal file
View file

@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}

19
docs/openapi/index.html Normal file
View file

@ -0,0 +1,19 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LibrePages Conductor | Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/docs/openapi/swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="/docs/openapi/index.css" />
<link rel="icon" type="image/png" href="/docs/openapi/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/docs/openapi/favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="/docs/openapi/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="/docs/openapi/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="/docs/openapi/swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>

View file

@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>

178
docs/openapi/openapi.yml Normal file
View file

@ -0,0 +1,178 @@
openapi: 3.0.3
info:
title: LibrePages Conductor - OpenAPI 3.0
description: |-
Conductor is the deployment manager used internally in LibrePages. It is
responsible for creating, updating and deleting websites that are deployed
with LibrePages
Some useful links:
- [LibrePages Conductor repository](https://git.batsense.net/LibrePages/conductor)
termsOfService: http://libreapages.org/terms/
contact:
email: contact@libreapages.org
license:
name: AGPLv3 or later version
url: https://www.gnu.org/licenses/agpl.html
version: 0.1.0
externalDocs:
description: LibrePages Conductor - internal service to update deployments
url: http://git.batsense.net/LibrePages/conductor
tags:
- name: meta
description: Information about the system
- name: site
description: Information about customer site deployments
paths:
/api/v1/events/new:
post:
tags:
- site
summary: Post new event to Conductor
description: Conductor schedules jobs based on events posted to it.
operationId: eventsNew
responses:
"201":
description: Successful operation
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/eventsNewPayloadNewSite"
- $ref: "#/components/schemas/eventsNewPayloadConfig"
- $ref: "#/components/schemas/eventsNewPayloadDeleteSite"
/api/v1/meta/build:
get:
tags:
- meta
summary: Get binary's build information
description: Update an existing pet by Idinformation
operationId: metaBuild
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/BuildInformation"
/api/v1/meta/health:
get:
tags:
- meta
summary: Get instance's health information
description: Get instance's health information
operationId: metaHealth
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/HealthInformation"
components:
schemas:
BuildInformation:
required:
- version
- git_commit_hash
- source_code
type: object
properties:
version:
type: string
example: v0.1.0
git_commit_hash:
type: string
example: 1fa28ef9b70bb04d6c76eee9e9bc5be77005b4b0
source_code:
type: string
example: https://git.batsense.net/LibrePages
HealthInformation:
required:
- conductor
type: object
properties:
conductor:
type: boolean
example: true
eventsNewPayloadConfig:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/LibConfigConfig'
LibConfigConfig:
properties:
source:
$ref: '#/components/schemas/LibConfigSource'
forms:
$ref: '#/components/schemas/LibConfigForms'
domains:
type: array
items:
type: string
example: ["example.com", "testing.example.org"]
image_compression:
$ref: '#/components/schemas/LibConfigImageCompression'
redirects:
$ref: '#/components/schemas/LibConfigRedirects'
LibConfigSource:
properties:
production_branch:
type: string
example: "librepages"
staging_branch:
type: string
example: "librepages-staging"
LibConfigForms:
properties:
enabled:
type: boolean
example: false
LibConfigImageCompression:
properties:
enabled:
type: boolean
example: false
LibConfigRedirects:
properties:
from:
type: string
example: "/from"
to:
type: string
example: "/to"
eventsNewPayloadNewSite:
properties:
hostname:
type: string
example: "example.org"
path:
type: string
example: "/tmp/example.org"
branch:
type: string
example: "librepages"
eventsNewPayloadDeleteSite:
properties:
hostname:
type: string
example: "example.org"
securitySchemes:
basicAuth:
type: http
scheme: basic

View file

@ -0,0 +1,20 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "/docs/openapi/openapi.yml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
//</editor-fold>
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

128
env/dummy_conductor/Cargo.lock generated vendored
View file

@ -13,6 +13,12 @@ dependencies = [
"syn",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "dummy_conductor"
version = "0.1.0"
@ -21,6 +27,16 @@ dependencies = [
"libconductor",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
@ -29,15 +45,46 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "libc"
version = "0.2.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]]
name = "libconductor"
version = "0.1.0"
dependencies = [
"async-trait",
"libconfig",
"serde",
"serde_json",
]
[[package]]
name = "libconfig"
version = "0.1.0"
source = "git+https://git.batsense.net/librepages/libconfig#f54290c4bae26b51a4945e0bf812e2b99856963b"
dependencies = [
"serde",
]
[[package]]
name = "num_cpus"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "proc-macro2"
version = "1.0.46"
@ -104,8 +151,89 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
dependencies = [
"autocfg",
"num_cpus",
"pin-project-lite",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

View file

@ -10,5 +10,8 @@ serde = { version = "1", features=["derive"]}
serde_json = { version ="1", features = ["raw_value"]}
async-trait = "0.1.57"
[dev-dependencies]
tokio = { version = "1.23.0", features = ["rt-multi-thread", "macros", "rt"] }
[dependencies.libconductor]
path = "../libconductor"

View file

@ -41,8 +41,8 @@ mod tests {
use super::*;
#[test]
fn all_good() {
#[tokio::test]
async fn all_good() {
let c = DummyConductor {};
assert_eq!(c.name(), DUMMY_CONDUCTOR_NAME);
assert!(c.health().await);

9
env/libconductor/Cargo.lock generated vendored
View file

@ -24,10 +24,19 @@ name = "libconductor"
version = "0.1.0"
dependencies = [
"async-trait",
"libconfig",
"serde",
"serde_json",
]
[[package]]
name = "libconfig"
version = "0.1.0"
source = "git+https://git.batsense.net/librepages/libconfig#f54290c4bae26b51a4945e0bf812e2b99856963b"
dependencies = [
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.46"

View file

@ -9,6 +9,7 @@ edition = "2021"
serde = { version = "1", features=["derive"]}
serde_json = { version ="1", features = ["raw_value"]}
async-trait = { version = "0.1.57", optional = true}
libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" }
[features]
default = [

View file

@ -14,10 +14,22 @@
* 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 libconfig::Config as LibConfig;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum EventType {
NewHostname(String),
NewSite {
path: String,
branch: String,
hostname: String,
},
DeleteSite {
hostname: String,
},
Config {
data: LibConfig,
},
}

117
scripts/bin-publish.sh Executable file
View file

@ -0,0 +1,117 @@
#!/bin/bash
# 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/>.
# publish.sh: grab bin from docker container, pack, sign and upload
# $2: binary version
# $3: Docker img tag
# $4: dumbserve password
set -xEeuo pipefail
DUMBSERVE_USERNAME=librepages
DUMBSERVE_PASSWORD=$4
DUMBSERVE_HOST="https://$DUMBSERVE_USERNAME:$DUMBSERVE_PASSWORD@dl.librepages.org"
NAME=conductor
KEY=67880CA5F4BC99BF247330E2DA576B07BC323961
TMP_DIR=$(mktemp -d)
FILENAME="$NAME-$2-linux-amd64"
TARBALL=$FILENAME.tar.gz
TARGET_DIR="$TMP_DIR/$FILENAME/"
mkdir -p $TARGET_DIR
DOCKER_IMG="realaravinth/$NAME:$3"
get_bin(){
cp target/release/conductor $TARGET_DIR
cp -r config/ $TARGET_DIR
cp -r contrib/ $TARGET_DIR
}
copy() {
echo "[*] Copying dist assets"
cp README.md $TARGET_DIR
cp LICENSE.md $TARGET_DIR
mkdir $TARGET_DIR/docs
# cp docs/CONFIGURATION.md $TARGET_DIR/docs
# cp -r docs/installation/ $TARGET_DIR/docs
get_bin
}
pack() {
echo "[*] Creating dist tarball"
pushd $TMP_DIR
tar -cvzf $TARBALL $FILENAME
popd
}
checksum() {
echo "[*] Generating dist tarball checksum"
pushd $TMP_DIR
sha256sum $TARBALL > $TARBALL.sha256
popd
}
sign() {
echo "[*] Signing dist tarball checksum"
pushd $TMP_DIR
export GPG_TTY=$(tty)
gpg --verbose \
--pinentry-mode loopback \
--batch --yes \
--passphrase $GPG_PASSWORD \
--local-user $KEY \
--output $TARBALL.asc \
--sign --detach \
--armor $TARBALL
popd
}
delete_dir() {
curl --location --request DELETE "$DUMBSERVE_HOST/api/v1/files/delete" \
--header 'Content-Type: application/json' \
--data-raw "{
\"path\": \"$1\"
}"
}
upload_dist() {
upload_dist="conductor/$1"
delete_dir $upload_dist
pushd $TMP_DIR
for file in $TARBALL $TARBALL.asc $TARBALL.sha256
do
curl -v \
-F upload=@$file \
"$DUMBSERVE_HOST/api/v1/files/upload?path=$upload_dist/"
done
popd
}
publish() {
copy
pack
checksum
sign
upload_dist $2
}
$1 $@

View file

@ -17,7 +17,7 @@
use actix_web::dev::ServiceRequest;
use actix_web::web;
use actix_web::Error;
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::extractors::bearer::BearerAuth;
use crate::errors::*;
use crate::AppCtx;
@ -26,14 +26,13 @@ use crate::SETTINGS;
pub mod meta;
pub mod webhook;
pub async fn httpauth(
pub async fn bearerauth(
req: ServiceRequest,
credentials: BasicAuth,
credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let _ctx: &AppCtx = req.app_data().unwrap();
let username = credentials.user_id();
let password = credentials.password().unwrap();
if SETTINGS.authenticate(username, password) {
let token = credentials.token();
if SETTINGS.authenticate(token) {
Ok(req)
} else {
let e = Error::from(ServiceError::Unauthorized);

View file

@ -24,7 +24,7 @@ use crate::errors::*;
use crate::AppCtx;
use crate::*;
use super::httpauth;
use super::bearerauth;
pub mod routes {
use super::*;
@ -47,7 +47,7 @@ pub fn services(cfg: &mut web::ServiceConfig) {
#[actix_web_codegen_const_routes::post(
path = "API_V1_ROUTES.webhook.post_event",
wrap = "HttpAuthentication::basic(httpauth)"
wrap = "HttpAuthentication::bearer(bearerauth)"
)]
async fn post_event(ctx: AppCtx, payload: web::Json<EventType>) -> ServiceResult<impl Responder> {
ctx.conductor.process(payload.into_inner()).await;
@ -56,9 +56,8 @@ async fn post_event(ctx: AppCtx, payload: web::Json<EventType>) -> ServiceResult
#[cfg(test)]
pub mod tests {
use actix_web::{http::StatusCode, test, App};
use super::*;
use actix_web::{http::StatusCode, test, App};
#[actix_rt::test]
async fn submit_works() {
@ -71,13 +70,14 @@ pub mod tests {
)
.await;
let creds = settings.api_keys.get(0).unwrap().clone();
let auth = format!(
"Basic {}",
base64::encode(format!("{}:{}", creds.username.clone(), creds.password))
);
let creds = settings.creds.clone();
let auth = format!("Bearer {}", creds.token,);
let new_hostname = EventType::NewHostname("demo.librepages.org".into());
let msg = EventType::NewSite {
hostname: "demo.librepages.org".into(),
branch: "librepages".into(),
path: "/tmp/librepages".into(),
};
// upload json
let upload_json = test::call_service(
@ -85,7 +85,7 @@ pub mod tests {
test::TestRequest::post()
.append_header((actix_web::http::header::AUTHORIZATION, auth.clone()))
.uri(API_V1_ROUTES.webhook.post_event)
.set_json(&new_hostname)
.set_json(&msg)
.to_request(),
)
.await;

View file

@ -37,13 +37,9 @@ impl Ctx {
pub async fn new(s: &Settings) -> ArcCtx {
let source_code = {
let mut url = s.source_code.clone();
if !url.ends_with('/') {
url.push('/');
}
let mut base = url::Url::parse(&url).unwrap();
base = base.join("tree/").unwrap();
base = base.join(crate::GIT_COMMIT_HASH).unwrap();
base.into()
url = url.join("tree/").unwrap();
url = url.join(crate::GIT_COMMIT_HASH).unwrap();
url.into()
};
let conductor: Box<dyn Conductor> = match s.conductor {

128
src/docs.rs Normal file
View file

@ -0,0 +1,128 @@
/*
* 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::borrow::Cow;
use actix_web::body::BoxBody;
use actix_web::{http::header, web, HttpResponse, Responder};
use mime_guess::from_path;
use rust_embed::RustEmbed;
use crate::CACHE_AGE;
pub const DOCS: routes::Docs = routes::Docs::new();
pub mod routes {
pub struct Docs {
pub home: &'static str,
pub spec: &'static str,
pub assets: &'static str,
}
impl Docs {
pub const fn new() -> Self {
Docs {
home: "/docs/openapi",
spec: "/docs/openapi/openapi.yml",
assets: "/docs/openapi/{_:.*}",
}
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(index).service(spec).service(dist);
}
#[derive(RustEmbed)]
#[folder = "docs/openapi/"]
struct Asset;
pub fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => {
let body: BoxBody = match content.data {
Cow::Borrowed(bytes) => BoxBody::new(bytes),
Cow::Owned(bytes) => BoxBody::new(bytes),
};
HttpResponse::Ok()
.insert_header(header::CacheControl(vec![
header::CacheDirective::Public,
header::CacheDirective::Extension("immutable".into(), None),
header::CacheDirective::MaxAge(CACHE_AGE),
]))
.content_type(from_path(path).first_or_octet_stream().as_ref())
.body(body)
}
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
#[actix_web_codegen_const_routes::get(path = "DOCS.assets")]
async fn dist(path: web::Path<String>) -> impl Responder {
handle_embedded_file(&path)
}
const OPEN_API_SPEC: &str = include_str!("../docs/openapi/openapi.yml");
#[actix_web_codegen_const_routes::get(path = "DOCS.spec")]
async fn spec() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/yaml")
.body(OPEN_API_SPEC)
}
#[actix_web_codegen_const_routes::get(path = "DOCS.home")]
async fn index() -> HttpResponse {
handle_embedded_file("index.html")
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use super::*;
use crate::*;
#[actix_rt::test]
async fn docs_works() {
const FILE: &str = "openapi.yml";
let app = test::init_service(
App::new()
.wrap(actix_web::middleware::NormalizePath::new(
actix_web::middleware::TrailingSlash::Trim,
))
.configure(services),
)
.await;
let resp =
test::call_service(&app, test::TestRequest::get().uri(DOCS.home).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
let resp =
test::call_service(&app, test::TestRequest::get().uri(DOCS.spec).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
let uri = format!("{}/{}", DOCS.home, "favicon-32x32.png");
println!("{uri}");
let resp = test::call_service(&app, test::TestRequest::get().uri(&uri).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}

View file

@ -19,6 +19,7 @@ use std::env;
use actix_web::http::StatusCode;
use actix_web::web::JsonConfig;
use actix_web::{error::InternalError, middleware, App, HttpServer};
use actix_web_prom::PrometheusMetricsBuilder;
use clap::{Parser, Subcommand};
use log::info;
@ -26,7 +27,7 @@ use lazy_static::lazy_static;
mod api;
mod ctx;
//mod docs;
mod docs;
#[cfg(not(tarpaulin_include))]
mod errors;
//#[macro_use]
@ -112,6 +113,11 @@ async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> {
let ip = settings.server.get_ip();
println!("Starting server on: http://{ip}");
let prometheus = PrometheusMetricsBuilder::new("api")
.endpoint("/metrics")
.build()
.unwrap();
HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
@ -124,6 +130,7 @@ async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> {
middleware::TrailingSlash::Trim,
))
.app_data(get_json_err())
.wrap(prometheus.clone())
.configure(routes::services)
})
.bind(ip)?

View file

@ -17,5 +17,6 @@
use actix_web::web;
pub fn services(cfg: &mut web::ServiceConfig) {
crate::docs::services(cfg);
crate::api::v1::services(cfg);
}

View file

@ -17,7 +17,7 @@
use std::env;
use std::path::Path;
use config::{Config, ConfigError, Environment, File};
use config::{builder::DefaultState, Config, ConfigBuilder, ConfigError, Environment, File};
use derive_more::Display;
use log::info;
use log::warn;
@ -54,57 +54,53 @@ pub enum ConductorType {
#[derive(Debug, Clone, Deserialize)]
pub struct Creds {
pub username: String,
pub password: String,
pub token: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Settings {
pub debug: bool,
pub api_keys: Vec<Creds>,
pub creds: Creds,
pub server: Server,
pub source_code: String,
pub source_code: Url,
pub conductor: ConductorType,
}
#[cfg(not(tarpaulin_include))]
impl Settings {
pub fn authenticate(&self, username: &str, password: &str) -> bool {
self.api_keys
.iter()
.any(|c| c.username == username && c.password == password)
pub fn authenticate(&self, token: &str) -> bool {
self.creds.token == token
}
pub fn new() -> Result<Self, ConfigError> {
let mut s = Config::new();
let mut s = Config::builder();
const CURRENT_DIR: &str = "./config/config.toml";
const ETC: &str = "/etc/lpconductor/config.toml";
const ETC: &str = "/etc/librepages/conductor/config.toml";
if let Ok(path) = env::var("LPCONDUCTOR_CONFIG") {
s.merge(File::with_name(&path))?;
s = s.add_source(File::with_name(&path));
} else if Path::new(CURRENT_DIR).exists() {
// merging default config from file
s.merge(File::with_name(CURRENT_DIR))?;
s = s.add_source(File::with_name(CURRENT_DIR));
} else if Path::new(ETC).exists() {
s.merge(File::with_name(ETC))?;
s = s.add_source(File::with_name(ETC));
} else {
warn!("configuration file not found");
}
s.merge(Environment::with_prefix(PREFIX).separator(SEPARATOR))?;
set_separator_field(&mut s);
check_url(&s);
s = s.add_source(Environment::with_prefix(PREFIX).separator(SEPARATOR));
s = set_separator_field(s);
match env::var("PORT") {
Ok(val) => {
s.set("server.port", val).unwrap();
s = s.set_override("server.port", val).unwrap();
}
Err(e) => warn!("couldn't interpret PORT: {}", e),
}
match s.try_into::<Self>() {
let s = s.build()?;
match s.try_deserialize::<Self>() {
Ok(val) => {
Ok(val)
},
@ -114,36 +110,41 @@ impl Settings {
}
#[cfg(not(tarpaulin_include))]
fn set_separator_field(s: &mut Config) {
fn set_separator_field(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
// ref: https://github.com/mehcode/config-rs/issues/391
fn from_env(s: &mut Config, env_name: &str, config_name: &str) {
fn from_env(
s: ConfigBuilder<DefaultState>,
env_name: &str,
config_name: &str,
) -> ConfigBuilder<DefaultState> {
if let Ok(val) = env::var(env_name) {
info!("Overriding {config_name} with data from env var {env_name}");
s.set(config_name, val)
.unwrap_or_else(|_| panic!("Couldn't set {config_name} from env var {env_name}"));
s.set_override(config_name, val)
.unwrap_or_else(|_| panic!("Couldn't set {config_name} from env var {env_name}"))
} else {
s
}
}
from_env(s, &format!("{PREFIX}{SEPARATOR}SOURCE_CODE"), "source_code");
from_env(
s = from_env(s, &format!("{PREFIX}{SEPARATOR}SOURCE_CODE"), "source_code");
s = from_env(
s,
&format!("{PREFIX}{SEPARATOR}SERVER{SEPARATOR}URL_PREFIX"),
"server.url_prefix",
);
from_env(
s = from_env(
s,
&format!("{PREFIX}{SEPARATOR}SERVER{SEPARATOR}PROXY_HAS_TLS"),
"server.proxy_has_tls",
);
}
#[cfg(not(tarpaulin_include))]
fn check_url(s: &Config) {
let url = s
.get::<String>("source_code")
.expect("Couldn't access source_code");
s = from_env(
s,
&format!("{PREFIX}{SEPARATOR}CREDS{SEPARATOR}TOKEN"),
"creds.token",
);
Url::parse(&url).expect("Please enter a URL for source_code in settings");
s
}
#[cfg(test)]
@ -153,16 +154,13 @@ mod tests {
#[test]
fn creds_works() {
let settings = Settings::new().unwrap();
let mut creds = settings.api_keys.get(0).unwrap().clone();
let creds = settings.creds.clone();
assert!(settings.authenticate(&creds.username, &creds.password));
assert!(settings.authenticate(&creds.token));
creds.username = "noexist".into();
assert!(!settings.authenticate(&creds.username, &creds.password));
let mut creds = settings.creds.clone();
let mut creds = settings.api_keys.get(0).unwrap().clone();
creds.password = "noexist".into();
assert!(!settings.authenticate(&creds.username, &creds.password));
creds.token = "noexist".into();
assert!(!settings.authenticate(&creds.token))
}
}