From 11ec15b07d2ff3a2342217e63587c505f29b7eb8 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 25 Mar 2025 09:01:56 +0100 Subject: [PATCH] feat: Implement catalog provider --- src/api/common.rs | 3 + src/api/error.rs | 7 + src/api/types.rs | 68 +++++++++ src/api/v3/auth/token/common.rs | 7 + src/api/v3/auth/token/mod.rs | 35 ++++- src/api/v3/auth/token/types.rs | 15 +- src/api/v3/role/mod.rs | 3 + src/api/v3/role_assignment/mod.rs | 3 + src/catalog/backends.rs | 16 +++ src/catalog/backends/error.rs | 47 ++++++ src/catalog/backends/sql.rs | 204 +++++++++++++++++++++++++++ src/catalog/backends/sql/endpoint.rs | 193 +++++++++++++++++++++++++ src/catalog/backends/sql/service.rs | 179 +++++++++++++++++++++++ src/catalog/error.rs | 57 ++++++++ src/catalog/mod.rs | 190 +++++++++++++++++++++++++ src/catalog/types.rs | 73 ++++++++++ src/catalog/types/endpoint.rs | 56 ++++++++ src/catalog/types/service.rs | 46 ++++++ src/config.rs | 10 ++ src/db/entity.rs | 26 ++++ src/error.rs | 7 + src/lib.rs | 1 + src/plugin_manager.rs | 9 ++ src/provider.rs | 10 ++ src/tests/api.rs | 5 + src/token/mod.rs | 4 +- 26 files changed, 1267 insertions(+), 7 deletions(-) create mode 100644 src/catalog/backends.rs create mode 100644 src/catalog/backends/error.rs create mode 100644 src/catalog/backends/sql.rs create mode 100644 src/catalog/backends/sql/endpoint.rs create mode 100644 src/catalog/backends/sql/service.rs create mode 100644 src/catalog/error.rs create mode 100644 src/catalog/mod.rs create mode 100644 src/catalog/types.rs create mode 100644 src/catalog/types/endpoint.rs create mode 100644 src/catalog/types/service.rs diff --git a/src/api/common.rs b/src/api/common.rs index 8f6cab7f..4fca5607 100644 --- a/src/api/common.rs +++ b/src/api/common.rs @@ -56,6 +56,7 @@ mod tests { use super::*; use crate::assignment::MockAssignmentProvider; + use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::MockIdentityProvider; use crate::keystone::Service; @@ -92,9 +93,11 @@ mod tests { let identity_mock = MockIdentityProvider::default(); let token_mock = MockTokenProvider::default(); let assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) diff --git a/src/api/error.rs b/src/api/error.rs index 6d16bbbd..6539b86e 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -22,6 +22,7 @@ use thiserror::Error; use tracing::error; use crate::assignment::error::AssignmentProviderError; +use crate::catalog::error::CatalogProviderError; use crate::identity::error::IdentityProviderError; use crate::resource::error::ResourceProviderError; use crate::token::error::TokenProviderError; @@ -65,6 +66,12 @@ pub enum KeystoneApiError { source: AssignmentProviderError, }, + #[error(transparent)] + CatalogError { + #[from] + source: CatalogProviderError, + }, + #[error(transparent)] IdentityError { #[from] diff --git a/src/api/types.rs b/src/api/types.rs index 963a20f3..07554d23 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -18,9 +18,12 @@ use axum::{ response::{IntoResponse, Response}, }; use chrono::{DateTime, Utc}; +use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use crate::catalog::types::{Endpoint as ProviderEndpoint, Service}; + #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] pub struct Versions { pub versions: Values, @@ -87,3 +90,68 @@ impl Default for MediaType { } } } + +/// A catalog object +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct Catalog(Vec); + +impl IntoResponse for Catalog { + fn into_response(self) -> Response { + (StatusCode::OK, Json(self)).into_response() + } +} + +/// A catalog object +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct CatalogService { + pub r#type: Option, + pub name: Option, + pub id: String, + pub endpoints: Vec, +} + +impl From<(Service, Vec)> for CatalogService { + fn from(value: (Service, Vec)) -> Self { + Self { + id: value.0.id.clone(), + name: value.0.name.clone(), + r#type: value.0.r#type, + endpoints: value.1.into_iter().map(Into::into).collect(), + } + } +} + +/// A Catalog Endpoint +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct Endpoint { + pub id: String, + pub url: String, + pub interface: String, + pub region: Option, + pub region_id: Option, +} + +impl From for Endpoint { + fn from(value: ProviderEndpoint) -> Self { + Self { + id: value.id.clone(), + interface: value.interface.clone(), + url: value.url.clone(), + region: value.region_id.clone(), + region_id: value.region_id.clone(), + } + } +} + +impl From)>> for Catalog { + fn from(value: Vec<(Service, Vec)>) -> Self { + Self( + value + .into_iter() + .map(|(srv, eps)| (srv, eps).into()) + .collect(), + ) + } +} diff --git a/src/api/v3/auth/token/common.rs b/src/api/v3/auth/token/common.rs index 94a287e3..0e6f332d 100644 --- a/src/api/v3/auth/token/common.rs +++ b/src/api/v3/auth/token/common.rs @@ -183,6 +183,7 @@ mod tests { MockAssignmentProvider, types::{Assignment, AssignmentType, Role as ProviderRole, RoleAssignmentListParameters}, }; + use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::{MockIdentityProvider, types::UserResponse}; use crate::keystone::Service; @@ -224,9 +225,11 @@ mod tests { }); let token_mock = MockTokenProvider::default(); let assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) @@ -277,9 +280,11 @@ mod tests { }); let token_mock = MockTokenProvider::default(); let assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) @@ -344,6 +349,7 @@ mod tests { }); let token_mock = MockTokenProvider::default(); let mut assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); assignment_mock.expect_list_role_assignments().returning( |_, _, q: &RoleAssignmentListParameters| { Ok(vec![Assignment { @@ -359,6 +365,7 @@ mod tests { let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) diff --git a/src/api/v3/auth/token/mod.rs b/src/api/v3/auth/token/mod.rs index aefaa70a..b9fe7ad3 100644 --- a/src/api/v3/auth/token/mod.rs +++ b/src/api/v3/auth/token/mod.rs @@ -12,12 +12,19 @@ // // SPDX-License-Identifier: Apache-2.0 -use axum::{Json, extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse}; +use axum::{ + Json, + extract::{Query, State}, + http::HeaderMap, + http::StatusCode, + response::IntoResponse, +}; use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use utoipa_axum::{router::OpenApiRouter, routes}; use uuid::Uuid; -use crate::api::{auth::Auth, common::get_domain, error::KeystoneApiError}; +use crate::api::{Catalog, auth::Auth, common::get_domain, error::KeystoneApiError}; +use crate::catalog::CatalogApi; use crate::identity::IdentityApi; use crate::identity::types::UserResponse; use crate::keystone::ServiceState; @@ -26,7 +33,7 @@ use crate::resource::{ types::{Domain, Project}, }; use crate::token::TokenApi; -use types::{AuthRequest, Scope, Token as ApiResponseToken, TokenResponse}; +use types::{AuthRequest, CreateTokenParameters, Scope, Token as ApiResponseToken, TokenResponse}; mod common; pub mod types; @@ -40,7 +47,7 @@ pub(super) fn openapi_router() -> OpenApiRouter { post, path = "/", description = "Issue token", - params(), + params(CreateTokenParameters), responses( (status = OK, description = "Token object", body = TokenResponse), ), @@ -48,6 +55,7 @@ pub(super) fn openapi_router() -> OpenApiRouter { )] #[tracing::instrument(name = "api::token_post", level = "debug", skip(state, req))] async fn post( + Query(query): Query, State(state): State, Json(req): Json, ) -> Result { @@ -160,7 +168,7 @@ async fn post( .expand_domain_information(&mut token, &state.db, &state.provider) .await?; - let api_token = TokenResponse { + let mut api_token = TokenResponse { token: ApiResponseToken::from_user_auth( &state, &token, @@ -170,6 +178,15 @@ async fn post( ) .await?, }; + if !query.nocatalog.is_some_and(|x| x) { + let catalog: Catalog = state + .provider + .get_catalog_provider() + .get_catalog(&state.db, true) + .await? + .into(); + api_token.token.catalog = Some(catalog); + } return Ok(( StatusCode::OK, [( @@ -260,6 +277,7 @@ mod tests { use super::openapi_router; use crate::api::v3::auth::token::types::TokenResponse; use crate::assignment::MockAssignmentProvider; + use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::{MockIdentityProvider, types::UserResponse}; use crate::keystone::Service; @@ -276,6 +294,7 @@ mod tests { let db = DatabaseConnection::Disconnected; let config = Config::default(); let assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let mut identity_mock = MockIdentityProvider::default(); identity_mock.expect_get_user().returning(|_, id: &'_ str| { Ok(Some(UserResponse { @@ -315,6 +334,7 @@ mod tests { let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) @@ -382,6 +402,7 @@ mod tests { let db = DatabaseConnection::Disconnected; let config = Config::default(); let mut assignment_mock = MockAssignmentProvider::default(); + let mut catalog_mock = MockCatalogProvider::default(); assignment_mock .expect_list_role_assignments() .returning(|_, _, _| Ok(Vec::new())); @@ -449,10 +470,14 @@ mod tests { token_mock .expect_encode_token() .returning(|_| Ok("token".to_string())); + catalog_mock + .expect_get_catalog() + .returning(|_, _| Ok(Vec::new())); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) diff --git a/src/api/v3/auth/token/types.rs b/src/api/v3/auth/token/types.rs index 41ef2be2..d58fea8f 100644 --- a/src/api/v3/auth/token/types.rs +++ b/src/api/v3/auth/token/types.rs @@ -20,9 +20,10 @@ use axum::{ use chrono::{DateTime, Utc}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; +use utoipa::{IntoParams, ToSchema}; use crate::api::error::TokenError; +use crate::api::types::Catalog; use crate::api::v3::role::types::Role; use crate::identity::types as identity_types; use crate::resource::types as resource_provider_types; @@ -74,6 +75,11 @@ pub struct Token { #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] pub roles: Option>, + + /// A catalog object. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub catalog: Option, } #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] @@ -267,3 +273,10 @@ impl TryFrom<&BackendToken> for Token { Ok(token.build()?) } } + +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +pub struct CreateTokenParameters { + /// The authentication response excludes the service catalog. By default, the response includes + /// the service catalog. + pub nocatalog: Option, +} diff --git a/src/api/v3/role/mod.rs b/src/api/v3/role/mod.rs index c038c9f2..edb4a045 100644 --- a/src/api/v3/role/mod.rs +++ b/src/api/v3/role/mod.rs @@ -116,6 +116,7 @@ mod tests { MockAssignmentProvider, types::{Role, RoleListParameters}, }; + use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::MockIdentityProvider; use crate::keystone::{Service, ServiceState}; @@ -136,11 +137,13 @@ mod tests { ..Default::default() })) }); + let catalog_mock = MockCatalogProvider::default(); let identity_mock = MockIdentityProvider::default(); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) diff --git a/src/api/v3/role_assignment/mod.rs b/src/api/v3/role_assignment/mod.rs index 8441be26..cc3237e6 100644 --- a/src/api/v3/role_assignment/mod.rs +++ b/src/api/v3/role_assignment/mod.rs @@ -88,6 +88,7 @@ mod tests { MockAssignmentProvider, types::{Assignment, AssignmentType, RoleAssignmentListParameters}, }; + use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::MockIdentityProvider; use crate::keystone::{Service, ServiceState}; @@ -107,10 +108,12 @@ mod tests { })) }); let identity_mock = MockIdentityProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) diff --git a/src/catalog/backends.rs b/src/catalog/backends.rs new file mode 100644 index 00000000..a4618644 --- /dev/null +++ b/src/catalog/backends.rs @@ -0,0 +1,16 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pub mod error; +pub mod sql; diff --git a/src/catalog/backends/error.rs b/src/catalog/backends/error.rs new file mode 100644 index 00000000..62a53b34 --- /dev/null +++ b/src/catalog/backends/error.rs @@ -0,0 +1,47 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +use crate::catalog::types::*; + +#[derive(Error, Debug)] +pub enum CatalogDatabaseError { + #[error("data serialization error")] + Serde { + #[from] + source: serde_json::Error, + }, + + #[error("database data")] + Database { + #[from] + source: sea_orm::DbErr, + }, + + #[error(transparent)] + EndpointBuilder { + #[from] + source: EndpointBuilderError, + }, + + #[error(transparent)] + ServiceBuilder { + #[from] + source: ServiceBuilderError, + }, + + #[error("service {0} not found")] + ServiceNotFound(String), +} diff --git a/src/catalog/backends/sql.rs b/src/catalog/backends/sql.rs new file mode 100644 index 00000000..a9dc60f0 --- /dev/null +++ b/src/catalog/backends/sql.rs @@ -0,0 +1,204 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use super::super::types::*; +use crate::catalog::CatalogProviderError; +use crate::catalog::backends::error::CatalogDatabaseError; +use crate::config::Config; +use crate::db::entity::{ + endpoint as db_endpoint, + prelude::{Endpoint as DbEndpoint, Service as DbService}, + service as db_service, +}; + +mod endpoint; +mod service; + +#[derive(Clone, Debug, Default)] +pub struct SqlBackend { + pub config: Config, +} + +impl SqlBackend {} + +#[async_trait] +impl CatalogBackend for SqlBackend { + /// Set config + fn set_config(&mut self, config: Config) { + self.config = config; + } + + /// List Services + #[tracing::instrument(level = "debug", skip(self, db))] + async fn list_services( + &self, + db: &DatabaseConnection, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError> { + Ok(service::list(&self.config, db, params).await?) + } + + /// Get single service by ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_service<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError> { + Ok(service::get(&self.config, db, id).await?) + } + + /// List Endpoints + #[tracing::instrument(level = "debug", skip(self, db))] + async fn list_endpoints( + &self, + db: &DatabaseConnection, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError> { + Ok(endpoint::list(&self.config, db, params).await?) + } + + /// Get single endpoint by ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_endpoint<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError> { + Ok(endpoint::get(&self.config, db, id).await?) + } + + /// Get Catalog (Services with Endpoints) + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_catalog( + &self, + db: &DatabaseConnection, + enabled: bool, + ) -> Result)>, CatalogProviderError> { + Ok(get_catalog(db, enabled).await?) + } +} + +async fn get_catalog( + db: &DatabaseConnection, + enabled: bool, +) -> Result)>, CatalogDatabaseError> { + let db_entities: Vec<(db_service::Model, Vec)> = DbService::find() + .filter(db_service::Column::Enabled.eq(enabled)) + .find_with_related(DbEndpoint) + .filter(db_endpoint::Column::Enabled.eq(enabled)) + .all(db) + .await?; + + let mut res: Vec<(Service, Vec)> = Vec::new(); + for (srv, db_endpoints) in db_entities.into_iter() { + let service: Service = srv.try_into()?; + let endpoints: Result, _> = db_endpoints + .into_iter() + .map(TryInto::::try_into) + .collect(); + res.push((service, endpoints?)); + } + Ok(res) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + use serde_json::json; + + use crate::db::entity::{endpoint, service}; + + use super::*; + + fn get_service_mock(id: String) -> service::Model { + service::Model { + id: id.clone(), + r#type: Some("type".into()), + enabled: true, + extra: Some(r#"{"name": "srv"}"#.to_string()), + } + } + + fn get_endpoint_mock(id: String) -> endpoint::Model { + endpoint::Model { + id: id.clone(), + interface: "public".into(), + service_id: "srv_id".into(), + region_id: Some("region".into()), + url: "http://localhost".into(), + enabled: true, + extra: None, + ..Default::default() + } + } + + #[tokio::test] + async fn test_get_catalog() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![ + (get_service_mock("1".into()), get_endpoint_mock("1".into())), + (get_service_mock("1".into()), get_endpoint_mock("2".into())), + ]]) + .into_connection(); + assert_eq!( + get_catalog(&db, false).await.unwrap(), + vec![( + Service { + id: "1".into(), + r#type: Some("type".into()), + enabled: true, + name: Some("srv".into()), + extra: Some(json!({"name": "srv"})), + }, + vec![ + Endpoint { + id: "1".into(), + interface: "public".into(), + service_id: "srv_id".into(), + region_id: Some("region".into()), + enabled: true, + url: "http://localhost".into(), + extra: None + }, + Endpoint { + id: "2".into(), + interface: "public".into(), + service_id: "srv_id".into(), + region_id: Some("region".into()), + enabled: true, + url: "http://localhost".into(), + extra: None + } + ] + )] + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "service"."id" AS "A_id", "service"."type" AS "A_type", "service"."enabled" AS "A_enabled", "service"."extra" AS "A_extra", "endpoint"."id" AS "B_id", "endpoint"."legacy_endpoint_id" AS "B_legacy_endpoint_id", "endpoint"."interface" AS "B_interface", "endpoint"."service_id" AS "B_service_id", "endpoint"."url" AS "B_url", "endpoint"."extra" AS "B_extra", "endpoint"."enabled" AS "B_enabled", "endpoint"."region_id" AS "B_region_id" FROM "service" LEFT JOIN "endpoint" ON "service"."id" = "endpoint"."service_id" WHERE "service"."enabled" = $1 AND "endpoint"."enabled" = $2 ORDER BY "service"."id" ASC"#, + [false.into(), false.into()] + ),] + ); + } +} diff --git a/src/catalog/backends/sql/endpoint.rs b/src/catalog/backends/sql/endpoint.rs new file mode 100644 index 00000000..39a237b5 --- /dev/null +++ b/src/catalog/backends/sql/endpoint.rs @@ -0,0 +1,193 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; +use serde_json::Value; + +use crate::catalog::backends::error::CatalogDatabaseError; +use crate::catalog::types::*; +use crate::config::Config; +use crate::db::entity::{endpoint as db_endpoint, prelude::Endpoint as DbEndpoint}; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + id: I, +) -> Result, CatalogDatabaseError> { + let select = DbEndpoint::find_by_id(id.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} + +pub async fn list( + _conf: &Config, + db: &DatabaseConnection, + params: &EndpointListParameters, +) -> Result, CatalogDatabaseError> { + let mut select = DbEndpoint::find(); + + if let Some(val) = ¶ms.interface { + select = select.filter(db_endpoint::Column::Interface.eq(val)); + } + if let Some(val) = ¶ms.service_id { + select = select.filter(db_endpoint::Column::ServiceId.eq(val)); + } + if let Some(val) = ¶ms.region_id { + select = select.filter(db_endpoint::Column::RegionId.eq(val)); + } + + let db_entities: Vec = select.all(db).await?; + let results: Result, _> = db_entities + .into_iter() + .map(TryInto::::try_into) + .collect(); + + results +} + +impl TryFrom for Endpoint { + type Error = CatalogDatabaseError; + + fn try_from(value: db_endpoint::Model) -> Result { + let mut builder = EndpointBuilder::default(); + builder.id(value.id.clone()); + builder.interface(value.interface.clone()); + builder.service_id(value.service_id.clone()); + builder.url(value.url.clone()); + builder.enabled(value.enabled); + if let Some(val) = &value.region_id { + builder.region_id(val); + } + if let Some(extra) = &value.extra { + let extra = serde_json::from_str::(extra).unwrap(); + builder.extra(extra); + } + + Ok(builder.build()?) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use crate::config::Config; + use crate::db::entity::endpoint; + + use super::*; + + fn get_endpoint_mock(id: String) -> endpoint::Model { + endpoint::Model { + id: id.clone(), + interface: "public".into(), + service_id: "srv_id".into(), + region_id: Some("region".into()), + url: "http://localhost".into(), + enabled: true, + extra: None, + ..Default::default() + } + } + + #[tokio::test] + async fn test_get() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([ + // First query result - select user itself + vec![get_endpoint_mock("1".into())], + ]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "1").await.unwrap().unwrap(), + Endpoint { + id: "1".into(), + interface: "public".into(), + service_id: "srv_id".into(), + region_id: Some("region".into()), + enabled: true, + url: "http://localhost".into(), + extra: None + } + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "endpoint"."id", "endpoint"."legacy_endpoint_id", "endpoint"."interface", "endpoint"."service_id", "endpoint"."url", "endpoint"."extra", "endpoint"."enabled", "endpoint"."region_id" FROM "endpoint" WHERE "endpoint"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ),] + ); + } + + #[tokio::test] + async fn test_list() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_endpoint_mock("1".into())]]) + .append_query_results([vec![get_endpoint_mock("1".into())]]) + .into_connection(); + let config = Config::default(); + assert!( + list(&config, &db, &EndpointListParameters::default()) + .await + .is_ok() + ); + assert_eq!( + list( + &config, + &db, + &EndpointListParameters { + interface: Some("public".into()), + service_id: Some("service_id".into()), + region_id: Some("region_id".into()) + } + ) + .await + .unwrap(), + vec![Endpoint { + id: "1".into(), + interface: "public".into(), + service_id: "srv_id".into(), + region_id: Some("region".into()), + enabled: true, + url: "http://localhost".into(), + extra: None + }] + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "endpoint"."id", "endpoint"."legacy_endpoint_id", "endpoint"."interface", "endpoint"."service_id", "endpoint"."url", "endpoint"."extra", "endpoint"."enabled", "endpoint"."region_id" FROM "endpoint""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "endpoint"."id", "endpoint"."legacy_endpoint_id", "endpoint"."interface", "endpoint"."service_id", "endpoint"."url", "endpoint"."extra", "endpoint"."enabled", "endpoint"."region_id" FROM "endpoint" WHERE "endpoint"."interface" = $1 AND "endpoint"."service_id" = $2 AND "endpoint"."region_id" = $3"#, + ["public".into(), "service_id".into(), "region_id".into()] + ), + ] + ); + } +} diff --git a/src/catalog/backends/sql/service.rs b/src/catalog/backends/sql/service.rs new file mode 100644 index 00000000..061d9a01 --- /dev/null +++ b/src/catalog/backends/sql/service.rs @@ -0,0 +1,179 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; +use serde_json::Value; + +use crate::catalog::backends::error::CatalogDatabaseError; +use crate::catalog::types::*; +use crate::config::Config; +use crate::db::entity::{prelude::Service as DbService, service as db_service}; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + id: I, +) -> Result, CatalogDatabaseError> { + let select = DbService::find_by_id(id.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} + +pub async fn list( + _conf: &Config, + db: &DatabaseConnection, + params: &ServiceListParameters, +) -> Result, CatalogDatabaseError> { + let mut select = DbService::find(); + + if let Some(typ) = ¶ms.r#type { + select = select.filter(db_service::Column::Type.eq(typ)); + } + + let db_services: Vec = select.all(db).await?; + let results: Result, _> = db_services + .into_iter() + .map(TryInto::::try_into) + .collect(); + + results +} + +impl TryFrom for Service { + type Error = CatalogDatabaseError; + + fn try_from(value: db_service::Model) -> Result { + let mut builder = ServiceBuilder::default(); + builder.id(value.id.clone()); + if let Some(typ) = &value.r#type { + builder.r#type(typ); + } + builder.enabled(value.enabled); + if let Some(extra) = &value.extra { + let extra = serde_json::from_str::(extra).unwrap(); + if let Some(name) = extra.get("name").and_then(|x| x.as_str()) { + builder.name(name); + } + builder.extra(extra); + } + + Ok(builder.build()?) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + use serde_json::json; + + use crate::config::Config; + use crate::db::entity::service; + + use super::*; + + fn get_service_mock(id: String) -> service::Model { + service::Model { + id: id.clone(), + r#type: Some("type".into()), + enabled: true, + extra: Some(r#"{"name": "srv"}"#.to_string()), + } + } + + #[tokio::test] + async fn test_get() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([ + // First query result - select user itself + vec![get_service_mock("1".into())], + ]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "1").await.unwrap().unwrap(), + Service { + id: "1".into(), + r#type: Some("type".into()), + enabled: true, + name: Some("srv".into()), + extra: Some(json!({"name": "srv"})), + } + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "service"."id", "service"."type", "service"."enabled", "service"."extra" FROM "service" WHERE "service"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ),] + ); + } + + #[tokio::test] + async fn test_list() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_service_mock("1".into())]]) + .append_query_results([vec![get_service_mock("1".into())]]) + .into_connection(); + let config = Config::default(); + assert!( + list(&config, &db, &ServiceListParameters::default()) + .await + .is_ok() + ); + assert_eq!( + list( + &config, + &db, + &ServiceListParameters { + r#type: Some("type".into()), + name: Some("service_name".into()) + } + ) + .await + .unwrap(), + vec![Service { + id: "1".into(), + r#type: Some("type".into()), + enabled: true, + name: Some("srv".into()), + extra: Some(json!({"name": "srv"})), + }] + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "service"."id", "service"."type", "service"."enabled", "service"."extra" FROM "service""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "service"."id", "service"."type", "service"."enabled", "service"."extra" FROM "service" WHERE "service"."type" = $1"#, + ["type".into()] + ), + ] + ); + } +} diff --git a/src/catalog/error.rs b/src/catalog/error.rs new file mode 100644 index 00000000..3644464a --- /dev/null +++ b/src/catalog/error.rs @@ -0,0 +1,57 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +use crate::catalog::backends::error::*; +use crate::catalog::types::ServiceBuilderError; + +#[derive(Error, Debug)] +pub enum CatalogProviderError { + /// Unsupported driver + #[error("unsupported driver {0}")] + UnsupportedDriver(String), + + /// Identity provider error + #[error("data serialization error")] + Serde { + #[from] + source: serde_json::Error, + }, + + /// Identity provider error + #[error(transparent)] + CatalogDatabase { + #[from] + source: CatalogDatabaseError, + }, + + #[error(transparent)] + ServiceBuilder { + #[from] + source: ServiceBuilderError, + }, + + #[error("service {0} not found")] + ServiceNotFound(String), +} + +impl CatalogProviderError { + pub fn database(source: CatalogDatabaseError) -> Self { + match source { + CatalogDatabaseError::ServiceNotFound(x) => Self::ServiceNotFound(x), + _ => Self::CatalogDatabase { source }, + } + } +} diff --git a/src/catalog/mod.rs b/src/catalog/mod.rs new file mode 100644 index 00000000..e471b410 --- /dev/null +++ b/src/catalog/mod.rs @@ -0,0 +1,190 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +#[cfg(test)] +use mockall::mock; +use sea_orm::DatabaseConnection; + +pub mod backends; +pub mod error; +pub(crate) mod types; + +use crate::catalog::backends::sql::SqlBackend; +use crate::catalog::error::CatalogProviderError; +use crate::catalog::types::{ + CatalogBackend, Endpoint, EndpointListParameters, Service, ServiceListParameters, +}; +use crate::config::Config; +use crate::plugin_manager::PluginManager; + +#[derive(Clone, Debug)] +pub struct CatalogProvider { + backend_driver: Box, +} + +#[async_trait] +pub trait CatalogApi: Send + Sync + Clone { + async fn list_services( + &self, + db: &DatabaseConnection, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError>; + + async fn get_service<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError>; + + async fn list_endpoints( + &self, + db: &DatabaseConnection, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError>; + + async fn get_endpoint<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError>; + + async fn get_catalog( + &self, + db: &DatabaseConnection, + enabled: bool, + ) -> Result)>, CatalogProviderError>; +} + +#[cfg(test)] +mock! { + pub CatalogProvider { + pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; + } + + #[async_trait] + impl CatalogApi for CatalogProvider { + async fn list_services( + &self, + db: &DatabaseConnection, + params: &ServiceListParameters + ) -> Result, CatalogProviderError>; + + async fn get_service<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError>; + + async fn list_endpoints( + &self, + db: &DatabaseConnection, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError>; + + async fn get_endpoint<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError>; + + async fn get_catalog( + &self, + db: &DatabaseConnection, + enabled: bool, + ) -> Result)>, CatalogProviderError>; + + } + + impl Clone for CatalogProvider { + fn clone(&self) -> Self; + } +} + +impl CatalogProvider { + pub fn new( + config: &Config, + plugin_manager: &PluginManager, + ) -> Result { + let mut backend_driver = if let Some(driver) = + plugin_manager.get_catalog_backend(config.catalog.driver.clone()) + { + driver.clone() + } else { + match config.resource.driver.as_str() { + "sql" => Box::new(SqlBackend::default()), + _ => { + return Err(CatalogProviderError::UnsupportedDriver( + config.resource.driver.clone(), + )); + } + } + }; + backend_driver.set_config(config.clone()); + Ok(Self { backend_driver }) + } +} + +#[async_trait] +impl CatalogApi for CatalogProvider { + /// List services + #[tracing::instrument(level = "info", skip(self, db))] + async fn list_services( + &self, + db: &DatabaseConnection, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError> { + self.backend_driver.list_services(db, params).await + } + + /// Get single service by ID + #[tracing::instrument(level = "info", skip(self, db))] + async fn get_service<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError> { + self.backend_driver.get_service(db, id).await + } + + /// List Endpoints + #[tracing::instrument(level = "info", skip(self, db))] + async fn list_endpoints( + &self, + db: &DatabaseConnection, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError> { + self.backend_driver.list_endpoints(db, params).await + } + + /// Get single endpoint by ID + #[tracing::instrument(level = "info", skip(self, db))] + async fn get_endpoint<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError> { + self.backend_driver.get_endpoint(db, id).await + } + + /// Get catalog + #[tracing::instrument(level = "info", skip(self, db))] + async fn get_catalog( + &self, + db: &DatabaseConnection, + enabled: bool, + ) -> Result)>, CatalogProviderError> { + self.backend_driver.get_catalog(db, enabled).await + } +} diff --git a/src/catalog/types.rs b/src/catalog/types.rs new file mode 100644 index 00000000..ea00bcc9 --- /dev/null +++ b/src/catalog/types.rs @@ -0,0 +1,73 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pub mod endpoint; +pub mod service; + +use async_trait::async_trait; +use dyn_clone::DynClone; +use sea_orm::DatabaseConnection; + +use crate::catalog::CatalogProviderError; +use crate::config::Config; + +pub use crate::catalog::types::endpoint::{ + Endpoint, EndpointBuilder, EndpointBuilderError, EndpointListParameters, +}; +pub use crate::catalog::types::service::{ + Service, ServiceBuilder, ServiceBuilderError, ServiceListParameters, +}; + +#[async_trait] +pub trait CatalogBackend: DynClone + Send + Sync + std::fmt::Debug { + /// Set config + fn set_config(&mut self, config: Config); + + /// List services + async fn list_services( + &self, + db: &DatabaseConnection, + params: &ServiceListParameters, + ) -> Result, CatalogProviderError>; + + /// Get single service by ID + async fn get_service<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError>; + + /// List Endpoints + async fn list_endpoints( + &self, + db: &DatabaseConnection, + params: &EndpointListParameters, + ) -> Result, CatalogProviderError>; + + /// Get single endpoint by ID + async fn get_endpoint<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, CatalogProviderError>; + + /// Get Catalog (Services with Endpoints) + async fn get_catalog( + &self, + db: &DatabaseConnection, + enabled: bool, + ) -> Result)>, CatalogProviderError>; +} + +dyn_clone::clone_trait_object!(CatalogBackend); diff --git a/src/catalog/types/endpoint.rs b/src/catalog/types/endpoint.rs new file mode 100644 index 00000000..25546b28 --- /dev/null +++ b/src/catalog/types/endpoint.rs @@ -0,0 +1,56 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct Endpoint { + /// The ID of the endpoint. + pub id: String, + /// The interface type, which describes the visibility of the endpoint. Value is: + /// - public. Visible by end users on a publicly available network interface. + /// + /// - internal. Visible by end users on an unmetered internal network interface. + /// + /// - admin. Visible by administrative users on a secure network interface. + #[builder(default)] + pub interface: String, + /// The ID of the region that contains the service endpoint. + #[builder(default)] + pub region_id: Option, + /// The UUID of the service to which the endpoint belongs. + pub service_id: String, + /// The endpoint URL. + pub url: String, + /// Indicates whether the endpoint appears in the service catalog: - false. The endpoint does + /// not appear in the service catalog. - true. The endpoint appears in the service catalog. + pub enabled: bool, + /// Additional endpoint properties + #[builder(default)] + pub extra: Option, +} + +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(setter(strip_option, into))] +pub struct EndpointListParameters { + /// Filters the response by an interface. + pub interface: Option, + /// Filters the response by a service ID. + pub service_id: Option, + /// Filters the response by a region ID. + pub region_id: Option, +} diff --git a/src/catalog/types/service.rs b/src/catalog/types/service.rs new file mode 100644 index 00000000..009c9aa8 --- /dev/null +++ b/src/catalog/types/service.rs @@ -0,0 +1,46 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct Service { + /// Additional service properties + #[builder(default)] + pub extra: Option, + /// The ID of the service. + pub id: String, + /// The service type. + #[builder(default)] + pub r#type: Option, + /// The service name. + #[builder(default)] + pub name: Option, + /// Defines whether the service and its endpoints appear in the service catalog: - false. The + /// service and its endpoints do not appear in the service catalog. - true. The service and its + /// endpoints appear in the service catalog. + pub enabled: bool, +} + +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(setter(strip_option, into))] +pub struct ServiceListParameters { + /// Filters the response by a service name. + pub name: Option, + /// Filters the response by a service type. A valid value is compute, ec2, identity, image, network, or volume. + pub r#type: Option, +} diff --git a/src/config.rs b/src/config.rs index e55a337e..4b3149f7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,10 @@ pub struct Config { #[serde(default)] pub auth: AuthSection, + /// Catalog + #[serde(default)] + pub catalog: CatalogSection, + /// Fernet tokens #[serde(default)] pub fernet_tokens: FernetTokenSection, @@ -107,6 +111,11 @@ pub struct AssignmentSection { pub driver: String, } +#[derive(Debug, Default, Deserialize, Clone)] +pub struct CatalogSection { + pub driver: String, +} + #[derive(Debug, Default, Deserialize, Clone)] pub struct IdentitySection { #[serde(default = "default_identity_driver")] @@ -181,6 +190,7 @@ impl Config { .set_default("fernet_tokens.key_repository", "/etc/keystone/fernet-keys/")? .set_default("fernet_tokens.max_active_keys", "3")? .set_default("assignment.driver", "sql")? + .set_default("catalog.driver", "sql")? .set_default("resource.driver", "sql")? .set_default("token.expiration", "3600")?; if std::path::Path::new(&path).is_file() { diff --git a/src/db/entity.rs b/src/db/entity.rs index 12c5be48..4d2ba301 100644 --- a/src/db/entity.rs +++ b/src/db/entity.rs @@ -107,3 +107,29 @@ impl Default for local_user::Model { } } } + +impl Default for service::Model { + fn default() -> Self { + Self { + id: String::new(), + r#type: None, + enabled: false, + extra: None, + } + } +} + +impl Default for endpoint::Model { + fn default() -> Self { + Self { + id: String::new(), + legacy_endpoint_id: None, + interface: String::new(), + service_id: String::new(), + url: String::new(), + enabled: false, + extra: None, + region_id: None, + } + } +} diff --git a/src/error.rs b/src/error.rs index f98d5ba0..efd3155c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,7 @@ use thiserror::Error; use crate::assignment::error::*; +use crate::catalog::error::*; use crate::identity::error::*; use crate::resource::error::*; use crate::token::TokenProviderError; @@ -27,6 +28,12 @@ pub enum KeystoneError { source: AssignmentProviderError, }, + #[error(transparent)] + CatalogError { + #[from] + source: CatalogProviderError, + }, + #[error(transparent)] IdentityError { #[from] diff --git a/src/lib.rs b/src/lib.rs index 5beb8d28..da64ee85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub mod api; pub mod assignment; +pub mod catalog; pub mod config; pub mod db; pub mod error; diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index 86b29b66..9e70d841 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -15,6 +15,7 @@ use std::collections::HashMap; use crate::assignment::types::AssignmentBackend; +use crate::catalog::types::CatalogBackend; use crate::identity::types::IdentityBackend; use crate::resource::types::ResourceBackend; @@ -24,6 +25,8 @@ use crate::resource::types::ResourceBackend; pub struct PluginManager { /// Assignments backend plugin assignment_backends: HashMap>, + /// Catalog backend plugins + catalog_backends: HashMap>, /// Identity backend plugins identity_backends: HashMap>, /// Resource backend plugins @@ -50,6 +53,12 @@ impl PluginManager { self.assignment_backends.get(name.as_ref()) } + /// Get registered catalog backend + #[allow(clippy::borrowed_box)] + pub fn get_catalog_backend>(&self, name: S) -> Option<&Box> { + self.catalog_backends.get(name.as_ref()) + } + /// Get registered identity backend #[allow(clippy::borrowed_box)] pub fn get_identity_backend>( diff --git a/src/provider.rs b/src/provider.rs index 5ee99240..076d81fd 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -17,6 +17,9 @@ use mockall_double::double; use crate::assignment::AssignmentApi; #[double] use crate::assignment::AssignmentProvider; +use crate::catalog::CatalogApi; +#[double] +use crate::catalog::CatalogProvider; use crate::config::Config; use crate::error::KeystoneError; use crate::identity::IdentityApi; @@ -42,6 +45,7 @@ use crate::token::TokenProvider; pub struct Provider { pub config: Config, assignment: AssignmentProvider, + catalog: CatalogProvider, identity: IdentityProvider, resource: ResourceProvider, token: TokenProvider, @@ -50,6 +54,7 @@ pub struct Provider { impl Provider { pub fn new(cfg: Config, plugin_manager: PluginManager) -> Result { let assignment_provider = AssignmentProvider::new(&cfg, &plugin_manager)?; + let catalog_provider = CatalogProvider::new(&cfg, &plugin_manager)?; let identity_provider = IdentityProvider::new(&cfg, &plugin_manager)?; let resource_provider = ResourceProvider::new(&cfg, &plugin_manager)?; let token_provider = TokenProvider::new(&cfg)?; @@ -57,6 +62,7 @@ impl Provider { Ok(Self { config: cfg, assignment: assignment_provider, + catalog: catalog_provider, identity: identity_provider, resource: resource_provider, token: token_provider, @@ -67,6 +73,10 @@ impl Provider { &self.assignment } + pub fn get_catalog_provider(&self) -> &impl CatalogApi { + &self.catalog + } + pub fn get_identity_provider(&self) -> &impl IdentityApi { &self.identity } diff --git a/src/tests/api.rs b/src/tests/api.rs index 2545f965..fc53783f 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -16,6 +16,7 @@ use sea_orm::DatabaseConnection; use std::sync::Arc; use crate::assignment::MockAssignmentProvider; +use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::MockIdentityProvider; use crate::keystone::{Service, ServiceState}; @@ -27,6 +28,7 @@ pub(crate) fn get_mocked_state_unauthed() -> ServiceState { let db = DatabaseConnection::Disconnected; let config = Config::default(); let assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let identity_mock = MockIdentityProvider::default(); let resource_mock = MockResourceProvider::default(); let mut token_mock = MockTokenProvider::default(); @@ -37,6 +39,7 @@ pub(crate) fn get_mocked_state_unauthed() -> ServiceState { let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) @@ -58,10 +61,12 @@ pub(crate) fn get_mocked_state(identity_mock: MockIdentityProvider) -> ServiceSt })) }); let assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock) diff --git a/src/token/mod.rs b/src/token/mod.rs index 97b94bb6..727d8f40 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -406,9 +406,9 @@ mod tests { MockAssignmentProvider, types::{Assignment, AssignmentType, Role, RoleAssignmentListParameters}, }; + use crate::catalog::MockCatalogProvider; use crate::config::Config; use crate::identity::MockIdentityProvider; - use crate::provider::ProviderBuilder; use crate::resource::MockResourceProvider; use crate::token::{ @@ -424,6 +424,7 @@ mod tests { let resource_mock = MockResourceProvider::default(); let token_mock = MockTokenProvider::default(); let mut assignment_mock = MockAssignmentProvider::default(); + let catalog_mock = MockCatalogProvider::default(); assignment_mock .expect_list_role_assignments() .withf(|_, _, q: &RoleAssignmentListParameters| { @@ -457,6 +458,7 @@ mod tests { let provider = ProviderBuilder::default() .config(config.clone()) .assignment(assignment_mock) + .catalog(catalog_mock) .identity(identity_mock) .resource(resource_mock) .token(token_mock)