From 8dcd4bfd3c7850d1e49e67ccd084abf85c8b46e8 Mon Sep 17 00:00:00 2001 From: Daniel Lundin Date: Wed, 11 Dec 2024 21:12:24 +0100 Subject: [PATCH] Add OpenAPI generation xtask --- .cargo/config.toml | 2 + .woodpecker/ci.yaml | 2 +- Cargo.lock | 10 +++ Cargo.toml | 8 +++ api.json | 84 ++++++++++++++++++++++++ controller/src/api.rs | 15 +++++ controller/src/bin/patagia-controller.rs | 78 ++-------------------- controller/src/context.rs | 13 ++++ controller/src/lib.rs | 4 ++ controller/src/version.rs | 49 ++++++++++++++ flake.nix | 21 +++++- justfile | 15 +++-- xtask/Cargo.toml | 15 +++++ xtask/src/main.rs | 30 +++++++++ 14 files changed, 268 insertions(+), 78 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 api.json create mode 100644 controller/src/api.rs create mode 100644 controller/src/context.rs create mode 100644 controller/src/version.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..54d6162 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --quiet --" diff --git a/.woodpecker/ci.yaml b/.woodpecker/ci.yaml index e8301ef..4a51387 100644 --- a/.woodpecker/ci.yaml +++ b/.woodpecker/ci.yaml @@ -8,4 +8,4 @@ steps: check: image: docker.io/nixpkgs/nix-flakes:nixos-24.11 commands: - - nix develop -c just check + - nix flake check diff --git a/Cargo.lock b/Cargo.lock index 1e35269..5545a04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2457,6 +2457,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "xtask" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "patagia-controller", + "semver", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 1a23a09..7e58724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,14 @@ resolver = "2" members = [ "agent", "controller", + "xtask", ] +default-members = [ + "agent", + "controller", + "xtask", +] + [workspace.package] version = "0.2.0" @@ -30,6 +37,7 @@ opentelemetry_sdk = { version = "0.27.1", features = ["metrics", "rt-tokio"] } opentelemetry-semantic-conventions = "0.27.0" opentelemetry-stdout = "0.27.0" schemars = "0.8.21" +semver = "1.0.23" serde = "1.0.215" slog = "2.7.0" slog-async = "2.8.0" diff --git a/api.json b/api.json new file mode 100644 index 0000000..fae6d29 --- /dev/null +++ b/api.json @@ -0,0 +1,84 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Patagia Controller", + "version": "1.0.0" + }, + "paths": { + "/version": { + "get": { + "summary": "Fetch version info.", + "operationId": "api_version", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "VersionInfo": { + "description": "Version and build information", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/controller/src/api.rs b/controller/src/api.rs new file mode 100644 index 0000000..4f45ccd --- /dev/null +++ b/controller/src/api.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use dropshot::ApiDescription; + +use std::sync::Arc; + +use crate::context::ControllerContext; +use crate::version; + +type ControllerApiDescription = ApiDescription>; + +pub fn api() -> Result { + let mut api = ControllerApiDescription::new(); + api.register(version::api_version)?; + Ok(api) +} diff --git a/controller/src/bin/patagia-controller.rs b/controller/src/bin/patagia-controller.rs index cf268ad..0a132f4 100644 --- a/controller/src/bin/patagia-controller.rs +++ b/controller/src/bin/patagia-controller.rs @@ -1,67 +1,22 @@ use anyhow::{anyhow, Result}; use clap::Parser; -use dropshot::{ - endpoint, ApiDescription, ConfigDropshot, HttpError, HttpResponseOk, RequestContext, - ServerBuilder, -}; -use schemars::JsonSchema; -use serde::Serialize; +use dropshot::{ConfigDropshot, ServerBuilder}; + use slog::Drain; -use tracing::Instrument; use tracing_slog::TracingSlogDrain; use std::net::SocketAddr; use std::str::FromStr; use std::sync::Arc; +use patagia_controller::api; +use patagia_controller::context::ControllerContext; use patagia_controller::instrumentation; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Cli {} -/// Represents a project in our API. -#[derive(Serialize, JsonSchema)] -struct VersionInfo { - name: String, - version: String, -} - -/// Fetch version info. -#[endpoint { - method = GET, - path = "/version", -}] -#[tracing::instrument( - skip(rqctx), - fields( - http.method=rqctx.request.method().as_str(), - http.path=rqctx.request.uri().path(), - http.remote_ip=rqctx.request.remote_addr().ip().to_string(), - request_id = rqctx.request_id, - ), - err(Debug), -)] -async fn api_version( - rqctx: RequestContext>, -) -> Result, HttpError> { - let ver = VersionInfo { - name: env!("CARGO_PKG_NAME").to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }; - - tracing::info_span!("Hello, span!"); - - async move { - tracing::info!("Someone made a request to /version"); - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - } - .instrument(tracing::info_span!("Let's do the thing....")) - .await; - Ok(HttpResponseOk(ver)) -} - #[tokio::main] async fn main() -> Result<()> { let _args = Cli::parse(); @@ -69,15 +24,6 @@ async fn main() -> Result<()> { tracing::info!("Patagia Controller"); - tracing::debug!("Starting server"); - - tracing::error!(name: "my-event-name", target: "my-system", event_id = 20, user_name = "otel", user_email = "otel@opentelemetry.io", message = "This is an example message"); - - foo().await; - - let mut api = ApiDescription::new(); - api.register(api_version).unwrap(); - let config = ConfigDropshot { bind_address: SocketAddr::from_str("0.0.0.0:9474").unwrap(), ..Default::default() @@ -90,22 +36,12 @@ async fn main() -> Result<()> { slog::Logger::root(async_drain, slog::o!()) }; - ServerBuilder::new(api, Arc::new(()), logger) + let ctx = ControllerContext::new(); + let api = api::api()?; + ServerBuilder::new(api, Arc::new(ctx), logger) .config(config) .start() .map_err(|e| anyhow!("Error starting server: {:?}", e))? .await .map_err(|e| anyhow!(e)) } - -#[tracing::instrument] -async fn foo() { - tracing::info!( - monotonic_counter.foo = 1_u64, - key_1 = "bar", - key_2 = 10, - "handle foo", - ); - - tracing::info!(histogram.baz = 10, "histogram example",); -} diff --git a/controller/src/context.rs b/controller/src/context.rs new file mode 100644 index 0000000..d994d44 --- /dev/null +++ b/controller/src/context.rs @@ -0,0 +1,13 @@ +pub struct ControllerContext {} + +impl ControllerContext { + pub fn new() -> ControllerContext { + ControllerContext {} + } +} + +impl Default for ControllerContext { + fn default() -> Self { + Self::new() + } +} diff --git a/controller/src/lib.rs b/controller/src/lib.rs index 9b48f64..9031343 100644 --- a/controller/src/lib.rs +++ b/controller/src/lib.rs @@ -1 +1,5 @@ +pub mod api; +pub mod context; pub mod instrumentation; + +mod version; diff --git a/controller/src/version.rs b/controller/src/version.rs new file mode 100644 index 0000000..3092a0c --- /dev/null +++ b/controller/src/version.rs @@ -0,0 +1,49 @@ +use dropshot::{endpoint, HttpError, HttpResponseOk, RequestContext}; +use schemars::JsonSchema; +use serde::Serialize; +use tracing::Instrument; + +use std::sync::Arc; + +use crate::context::ControllerContext; + +/// Version and build information +#[derive(Serialize, JsonSchema)] +struct VersionInfo { + name: String, + version: String, +} + +/// Fetch version info. +#[endpoint { + method = GET, + path = "/version", +}] +#[tracing::instrument( + skip(rqctx), + fields( + http.method=rqctx.request.method().as_str(), + http.path=rqctx.request.uri().path(), + http.remote_ip=rqctx.request.remote_addr().ip().to_string(), + request_id = rqctx.request_id, + ), + err(Debug), +)] +pub async fn api_version( + rqctx: RequestContext>, +) -> Result, HttpError> { + let ver = VersionInfo { + name: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + + tracing::info_span!("Hello, span!"); + + async move { + tracing::info!("Someone made a request to /version"); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + .instrument(tracing::info_span!("Let's do the thing....")) + .await; + Ok(HttpResponseOk(ver)) +} diff --git a/flake.nix b/flake.nix index 4077313..8f14c2f 100644 --- a/flake.nix +++ b/flake.nix @@ -87,10 +87,12 @@ pkgs.lib.fileset.toSource { root = ./.; fileset = pkgs.lib.fileset.unions [ + ./api.json ./Cargo.toml ./Cargo.lock (craneLib.fileset.commonCargoSources ./agent) (craneLib.fileset.commonCargoSources ./controller) + (craneLib.fileset.commonCargoSources ./xtask) (craneLib.fileset.commonCargoSources crate) ]; }; @@ -113,16 +115,24 @@ } ); + xtask = craneLib.buildPackage ( + individualCrateArgs + // { + pname = "xtask"; + cargoExtraArgs = "-p xtask"; + src = fileSetForCrate ./xtask; + } + ); in { # `nix build` packages = { - inherit patagia-agent patagia-controller; + inherit patagia-agent patagia-controller xtask; }; # Tests checks = { - inherit patagia-agent patagia-controller; + inherit patagia-agent patagia-controller xtask; clippy = craneLib.cargoClippy ( commonArgs @@ -144,6 +154,13 @@ partitionType = "count"; } ); + + openapi = + pkgs.runCommand "openapi" (commonArgs // { + src = fileSetForCrate ./xtask; + }) '' + ${self.packages.${system}.xtask}/bin/xtask open-api | ${pkgs.diffutils}/bin/diff -u $src/api.json - | tee $out + ''; }; # For `nix fmt` diff --git a/justfile b/justfile index ce7b418..893e8ae 100644 --- a/justfile +++ b/justfile @@ -12,10 +12,6 @@ run-controller $RUST_LOG="debug,h2=info,hyper_util=info,tower=info": dev-controller: watchexec --clear --restart --stop-signal INT --debounce 300ms -- just run-controller -# Run all tests -check: - nix flake check - # Lint all source code lint: cargo clippy @@ -39,3 +35,14 @@ update-nix: # Find unused dependencies with cargo machete machete: cargo machete + +# Generate OpenAPI spec +open-api: + cargo xtask open-api + +# Run all tests +check: check-nix + +# check-nix +check-nix: + nix flake check diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..848c00e --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "xtask" +version = "0.0.0" +edition = "2021" + +[[bin]] +name = "xtask" +test = false +doctest = false + +[dependencies] +anyhow.workspace = true +clap = { workspace = true, features = ["derive"] } +patagia-controller = { path = "../controller" } +semver.workspace = true diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..7909d65 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,30 @@ +use anyhow::Result; +use clap::Parser; + +use patagia_controller::api; + +#[derive(Debug, Parser)] +#[command(name = "cargo xtask", about = "Extra tasks for Patagia Controller")] +enum Xtask { + /// Generate OpenAPI spec + OpenApi, +} + +fn gen_openapi() -> Result<()> { + let api = api::api()?; + let openapi = api.openapi("Patagia Controller", semver::Version::new(1, 0, 0)); + openapi.write(&mut std::io::stdout().lock())?; + Ok(()) +} + +fn main() -> Result<()> { + let xtask = Xtask::parse(); + + match xtask { + Xtask::OpenApi => { + gen_openapi()?; + } + } + + Ok(()) +}