Add OpenAPI generation xtask
Some checks failed
ci/woodpecker/push/ci Pipeline failed

This commit is contained in:
Daniel Lundin 2024-12-11 21:12:24 +01:00
parent ac6a53fac9
commit 0528017a47
Signed by: dln
SSH key fingerprint: SHA256:dQy1Xj3UiqJYpKR5ggQ2bxgz4jCH8IF+k3AB8o0kmdI
14 changed files with 268 additions and 78 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --quiet --"

View file

@ -8,4 +8,4 @@ steps:
check:
image: docker.io/nixpkgs/nix-flakes:nixos-24.11
commands:
- nix develop -c just check
- nix flake check

10
Cargo.lock generated
View file

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

View file

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

84
api.json Normal file
View file

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

15
controller/src/api.rs Normal file
View file

@ -0,0 +1,15 @@
use anyhow::Result;
use dropshot::ApiDescription;
use std::sync::Arc;
use crate::context::ControllerContext;
use crate::version;
type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
pub fn api() -> Result<ControllerApiDescription> {
let mut api = ControllerApiDescription::new();
api.register(version::api_version)?;
Ok(api)
}

View file

@ -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<Arc<()>>,
) -> Result<HttpResponseOk<VersionInfo>, 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",);
}

13
controller/src/context.rs Normal file
View file

@ -0,0 +1,13 @@
pub struct ControllerContext {}
impl ControllerContext {
pub fn new() -> ControllerContext {
ControllerContext {}
}
}
impl Default for ControllerContext {
fn default() -> Self {
Self::new()
}
}

View file

@ -1 +1,5 @@
pub mod api;
pub mod context;
pub mod instrumentation;
mod version;

49
controller/src/version.rs Normal file
View file

@ -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<Arc<ControllerContext>>,
) -> Result<HttpResponseOk<VersionInfo>, 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))
}

View file

@ -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 -
'';
};
# For `nix fmt`

View file

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

15
xtask/Cargo.toml Normal file
View file

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

30
xtask/src/main.rs Normal file
View file

@ -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(())
}