lenso-cli 0.2.25

Lenso command-line interface for scaffolding and operating Lenso backend projects.
use axum::http::{HeaderMap, StatusCode};
use axum::{Json, Router, extract::Path, routing::get};
use serde_json::{Value, json};
use sha2::{Digest as _, Sha256};

const SERVICE_ID: &str = "{{service_id}}";
const SERVICE_VERSION: &str = "0.1.0";
const MODULE_ID: &str = "{{module_name}}";
const MODULE_EXPORT: &str = "{{module_export}}";
const MODULE_VERSION: &str = "0.1.0";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenvy::from_filename(".env.local");
    if std::env::args().any(|arg| arg == "--check") {
        println!("{}", serde_json::to_string_pretty(&service_manifest())?);
        return Ok(());
    }
    if std::env::args().any(|arg| arg == "--check-release") {
        println!("{}", serde_json::to_string_pretty(&module_release())?);
        return Ok(());
    }

    let port = std::env::var("PORT")
        .ok()
        .and_then(|value| value.parse::<u16>().ok())
        .unwrap_or({{service_port}});
    let app = Router::new()
        .route("/lenso/service/v1/manifest", get(manifest))
        .route("/lenso/provider/v1", get(provider))
        .route(
            "/lenso/provider/v1/exports/{export}/module-release",
            get(module_release_endpoint),
        )
        .route("/system-plane/v1", get(system_plane_core))
        .route("/status", get(status))
        .merge(lenso_service::health_router());
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await?;

    println!("Lenso service ready: http://127.0.0.1:{port}/lenso/provider/v1");
    axum::serve(listener, app).await?;
    Ok(())
}

async fn manifest() -> Json<Value> {
    Json(service_manifest())
}

async fn provider() -> Json<Value> {
    Json(provider_descriptor())
}

async fn module_release_endpoint(
    Path(export): Path<String>,
) -> Result<Json<Value>, StatusCode> {
    if export != MODULE_EXPORT {
        return Err(StatusCode::NOT_FOUND);
    }
    Ok(Json(module_release()))
}

async fn status() -> Json<Value> {
    Json(json!({ "service": SERVICE_ID, "status": "ready" }))
}

async fn system_plane_core(headers: HeaderMap) -> Result<Json<Value>, StatusCode> {
    let expected = std::env::var("LENSO_LOCAL_ENROLLMENT_TOKEN")
        .map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
    let supplied = headers
        .get(axum::http::header::AUTHORIZATION)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;
    if supplied != expected {
        return Err(StatusCode::UNAUTHORIZED);
    }
    Ok(Json(json!({
        "protocol": lenso_service::system_plane::CORE_PROTOCOL,
        "serviceId": SERVICE_ID,
        "servicePrincipal": format!("service:{SERVICE_ID}"),
        "serviceRevision": "1",
        "capabilities": [],
    })))
}

fn service_manifest() -> Value {
    json!({
        "protocol": "lenso.service.v1",
        "name": "{{service_name}}",
        "version": SERVICE_VERSION,
        "provider": {
            "name": "{{service_name}}",
            "summary": "{{service_label}} provider",
        },
        "compatibility": {
            "providerProtocolVersion": "lenso.provider.v1",
            "requiredHostFeatures": ["service.status"],
        },
        "install": {
            "services": [
                {
                    "name": "{{service_name}}",
                    "command": "cargo run",
                    "cwd": {{service_cwd}},
                    "readyUrl": "{{service_status_url}}",
                    "autoStart": true,
                    "readyTimeoutMs": 300000,
                },
            ],
        },
        "modules": [module_manifest()],
    })
}

fn module_manifest() -> Value {
    json!({
        "capabilities": [format!("{MODULE_ID}.read")],
        "console": [],
        "console_contributions": [],
        "console_slots": [],
        "http_routes": [{
            "capability": format!("{MODULE_ID}.read"),
            "display_name": "Read service status",
            "method": "GET",
            "path": "/status",
            "story_title": "Service status read",
        }],
        "module_id": MODULE_ID,
        "protocol": "lenso.module-manifest.v1",
        "story_display": [],
    })
}

fn service_release_digest() -> String {
    digest(&json!({
        "modules": [MODULE_ID],
        "protocol": "lenso.provider-service-release.v1",
        "serviceId": SERVICE_ID,
        "version": SERVICE_VERSION,
    }))
}

fn operation_contract_digest() -> String {
    digest(&json!({
        "operations": ["GET /status"],
        "protocol": "lenso.provider-http.v1",
    }))
}

fn module_release() -> Value {
    let manifest = module_manifest();
    json!({
        "compatibility": {},
        "delivery": {
            "kind": "service",
            "service_id": SERVICE_ID,
            "service_release_version": SERVICE_VERSION,
            "service_release_digest": service_release_digest(),
            "export": MODULE_EXPORT,
            "responsibility_profile": "provider",
            "contract_digests": [operation_contract_digest()],
        },
        "manifest_digest": digest(&manifest),
        "manifest": manifest,
        "module_id": MODULE_ID,
        "protocol": "lenso.module-release.v1",
        "version": MODULE_VERSION,
    })
}

fn provider_descriptor() -> Value {
    let release = module_release();
    let manifest = module_manifest();
    json!({
        "exports": [{
            "contractDigests": { "http": operation_contract_digest() },
            "exportKey": MODULE_EXPORT,
            "manifest": manifest,
            "manifestDigest": release["manifest_digest"],
            "moduleId": MODULE_ID,
            "moduleReleaseDigest": digest(&release),
            "moduleVersion": MODULE_VERSION,
        }],
        "protocolContractDigest": digest(&json!({ "protocol": "lenso.provider.v1" })),
        "runtimeInstanceId": format!("{SERVICE_ID}-local"),
        "serviceId": SERVICE_ID,
        "serviceReleaseDigest": service_release_digest(),
        "serviceReleaseVersion": SERVICE_VERSION,
    })
}

fn digest(value: &Value) -> String {
    let bytes = serde_json_canonicalizer::to_vec(value).expect("service contract canonicalizes");
    let mut hex = String::with_capacity(64);
    for byte in Sha256::digest(bytes) {
        use std::fmt::Write as _;
        write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
    }
    format!("sha256:{hex}")
}