From 6cdf36a0088d3a7dc407db4af4389050d7df1afc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 25 Jul 2025 18:54:51 +0200 Subject: [PATCH] feat: Allow dumping OpenAPI directly from bin Allow dumping the OpenAPI spec directly from keystone executable instead of starting it. --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++ Cargo.lock | 20 +++++++++++++++ Cargo.toml | 2 +- src/bin/keystone.rs | 53 ++++++++++++++++++++++++++++++++-------- 4 files changed, 93 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b1e9819..7d1c4355 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,32 @@ jobs: - name: Run Doc tests run: cargo test --doc + + openapi: + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust + uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable + with: + toolchain: stable + targets: ${{ matrix.target }} + + - name: Rust Cache + uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + + - name: Generate OpenAPI + run: cargo run --bin keystone -- --dump-openapi yaml > openapi.yaml + + - name: Upload OpenAPI spec + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: openapi.spec + path: openapi.yaml diff --git a/Cargo.lock b/Cargo.lock index 2fa514bb..baea468b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4406,6 +4406,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_norway" +version = "0.9.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c" +dependencies = [ + "indexmap 2.10.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml-norway", +] + [[package]] name = "serde_path_to_error" version = "0.1.17" @@ -5509,6 +5522,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unsafe-libyaml-norway" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104" + [[package]] name = "untrusted" version = "0.9.0" @@ -5554,6 +5573,7 @@ dependencies = [ "indexmap 2.10.0", "serde", "serde_json", + "serde_norway", "utoipa-gen", ] diff --git a/Cargo.toml b/Cargo.toml index 4a1d9677..95640445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ tower-http = { version = "0.6", features = ["compression-full", "request-id", "s tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = [] } url = { version = "2.5", features = ["serde"] } -utoipa = { version = "5.4", features = ["axum_extras", "chrono"] } +utoipa = { version = "5.4", features = ["axum_extras", "chrono", "yaml"] } utoipa-axum = { version = "0.2" } utoipa-swagger-ui = { version = "9.0", features = ["axum", "vendored"], default-features = false } uuid = { version = "1.17", features = ["v4"] } diff --git a/src/bin/keystone.rs b/src/bin/keystone.rs index 86331bd4..a876926a 100644 --- a/src/bin/keystone.rs +++ b/src/bin/keystone.rs @@ -12,8 +12,12 @@ // // SPDX-License-Identifier: Apache-2.0 +//! Main Keystone executable. +//! +//! This is the entry point of the `keystone` binary. + use axum::http::{self, HeaderName, Request, header}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use color_eyre::eyre::{Report, Result}; use sea_orm::ConnectOptions; use sea_orm::Database; @@ -45,17 +49,33 @@ use openstack_keystone::plugin_manager::PluginManager; use openstack_keystone::policy::PolicyFactory; use openstack_keystone::provider::Provider; -/// Simple program to greet a person +/// OpenStack Keystone. +/// +/// Keystone is an OpenStack service that provides API client authentication, service discovery, +/// and distributed multi-tenant authorization by implementing OpenStack’s Identity API. #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { - /// Path to the keystone config file - #[arg(short, long)] - config: String, + /// Path to the keystone config file. + #[arg(short, long, required_unless_present("dump_openapi"))] + config: Option, /// Verbosity level. Repeat to increase level. #[arg(short, long, global=true, action = clap::ArgAction::Count, display_order = 920)] pub verbose: u8, + + /// Print the OpenAPI schema json instead of running the Keystone. + #[arg(long)] + pub dump_openapi: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, ValueEnum)] +enum OpenApiFormat { + /// Json. + Json, + #[default] + /// Yaml. + Yaml, } // A `MakeRequestId` that increments an atomic counter @@ -95,10 +115,27 @@ async fn main() -> Result<(), Report> { // build the tracing registry tracing_subscriber::registry().with(log_layer).init(); + let openapi = api::ApiDoc::openapi(); + + let (router, api) = OpenApiRouter::with_openapi(openapi.clone()) + .merge(api::openapi_router()) + .split_for_parts(); + + if let Some(dump_format) = &args.dump_openapi { + println!( + "{}", + match dump_format { + OpenApiFormat::Yaml => api.to_yaml()?, + OpenApiFormat::Json => api.to_pretty_json()?, + } + ); + return Ok(()); + } + let token = CancellationToken::new(); let cloned_token = token.clone(); - let cfg = Config::new(args.config.into())?; + let cfg = Config::new(args.config.expect("config file is required.").into())?; let db_url = cfg.database.get_connection(); let mut opt = ConnectOptions::new(db_url.to_owned()); if args.verbose < 2 { @@ -128,10 +165,6 @@ async fn main() -> Result<(), Report> { spawn(cleanup(cloned_token, shared_state.clone())); - let (router, api) = OpenApiRouter::with_openapi(api::ApiDoc::openapi()) - .merge(api::openapi_router()) - .split_for_parts(); - let x_request_id = HeaderName::from_static("x-openstack-request-id"); let sensitive_headers: Arc<[_]> = vec![ header::AUTHORIZATION,