diff --git a/databricks-kube/src/crdgen.rs b/databricks-kube/src/crdgen.rs index 32f6dc5..35f4ace 100644 --- a/databricks-kube/src/crdgen.rs +++ b/databricks-kube/src/crdgen.rs @@ -20,4 +20,8 @@ fn main() { "---\n{}\n", to_string(&crate::crds::repo::Repo::crd()).unwrap() ); + print!( + "---\n{}\n", + to_string(&crate::crds::databricks_secret_scope::DatabricksSecretScope::crd()).unwrap() + ); } diff --git a/databricks-kube/src/crds/databricks_secret_scope.rs b/databricks-kube/src/crds/databricks_secret_scope.rs new file mode 100644 index 0000000..fcdebb9 --- /dev/null +++ b/databricks-kube/src/crds/databricks_secret_scope.rs @@ -0,0 +1,135 @@ +use std::pin::Pin; +use std::{sync::Arc, time::SystemTime}; + +use crate::context::Context; +use async_stream::{stream, try_stream}; +use databricks_rust_secrets::apis::secret_api; +use databricks_rust_secrets::models::{ + WorkspaceCreateScope, WorkspaceDeleteScope, WorkspaceListScopesResponse, WorkspaceSecretScope, +}; +use futures::{future, Stream, StreamExt, TryStreamExt}; +use kube::{core::object::HasSpec, CustomResource}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::traits::rest_config::RestConfig; +use crate::{error::DatabricksKubeError, traits::remote_api_resource::RemoteAPIResource}; + +#[derive(Clone, CustomResource, Debug, Default, Deserialize, PartialEq, Serialize, JsonSchema)] +#[kube( + group = "com.dstancu.databricks", + version = "v1", + kind = "DatabricksSecretScope", + derive = "Default", + namespaced +)] +pub struct DatabricksSecretScopeSpec { + pub scope: WorkspaceSecretScope, +} + +// API -> CRD +impl From for DatabricksSecretScope { + fn from(scope: WorkspaceSecretScope) -> Self { + let k8s_name = scope.name.clone().unwrap_or(format!( + "noname-{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + )); + + Self::new(&k8s_name, DatabricksSecretScopeSpec { scope }) + } +} + +// CRD -> API +impl From for WorkspaceSecretScope { + fn from(value: DatabricksSecretScope) -> Self { + value.spec().scope.clone() + } +} + +impl RemoteAPIResource for DatabricksSecretScope { + fn remote_list_all( + context: Arc, + ) -> Pin> + Send>> { + try_stream! { + let config = WorkspaceListScopesResponse::get_rest_config(context.clone()).await.unwrap(); + + if let WorkspaceListScopesResponse { + scopes + } = secret_api::list_scopes(&config).await? { + if let Some(scopes) = scopes { + for scope in scopes { + yield scope; + } + } + } + } + .boxed() + } + + fn remote_get( + &self, + context: Arc, + ) -> Pin> + Send>> { + let scope_name = self.spec().scope.name.clone().unwrap(); + + stream! { + let remote = Self::remote_list_all(context) + .try_filter(move |rs| future::ready(rs.clone().name.map(|rsn| rsn == scope_name.clone()).unwrap_or(false))) + .next() + .await; + + yield remote.unwrap_or(Err(DatabricksKubeError::IDUnsetError)) + } + .boxed() + } + + fn remote_create( + &self, + context: Arc, + ) -> Pin> + Send + '_>> { + let scope = self.spec().scope.clone(); + + try_stream! { + let config = WorkspaceSecretScope::get_rest_config(context.clone()).await.unwrap(); + + // Endpoint has no response value + secret_api::create_scope(&config, Some(WorkspaceCreateScope { + scope: scope.name.unwrap(), + ..Default::default() + })).await?; + + yield self.clone() + } + .boxed() + } + + fn remote_update( + &self, + _: Arc, + ) -> Pin> + Send + '_>> { + log::warn!("No change made! Secret scope resources are not updatable. Delete and recreate your resource."); + try_stream! { yield self.clone() }.boxed() + } + + fn remote_delete( + &self, + context: Arc, + ) -> Pin> + Send + '_>> { + let scope = self.spec().scope.clone(); + + try_stream! { + let config = WorkspaceSecretScope::get_rest_config(context.clone()).await.unwrap(); + secret_api::delete_scope( + &config, + Some(WorkspaceDeleteScope { scope: scope.name.unwrap() }) + ).await?; + + yield (); + } + .boxed() + } +} diff --git a/databricks-kube/src/error.rs b/databricks-kube/src/error.rs index 470f1cb..17828d7 100644 --- a/databricks-kube/src/error.rs +++ b/databricks-kube/src/error.rs @@ -28,11 +28,13 @@ macro_rules! openapi_error_glue { status, content, entity, - }) => Self::APIError(OpenAPIError::ResponseError(SerializableResponseContent { - status, - content, - entity: entity.and_then(|e| to_value(e).ok()), - })), + }) => { + Self::APIError(OpenAPIError::ResponseError(SerializableResponseContent { + status, + content, + entity: entity.and_then(|e| to_value(e).ok()), + })) + } $error::Io(e) => Self::APIError(OpenAPIError::Io(e)), $error::Serde(e) => Self::APIError(OpenAPIError::Serde(e)), $error::Reqwest(e) => Self::APIError(OpenAPIError::Reqwest(e)), diff --git a/databricks-kube/src/main.rs b/databricks-kube/src/main.rs index 326ae43..af9289e 100644 --- a/databricks-kube/src/main.rs +++ b/databricks-kube/src/main.rs @@ -9,6 +9,7 @@ use std::{collections::BTreeMap, hash::Hash, sync::Arc, time::Duration}; use databricks_kube::{ context::Context, crds::databricks_job::DatabricksJob, + crds::databricks_secret_scope::DatabricksSecretScope, crds::git_credential::GitCredential, crds::repo::Repo, error::DatabricksKubeError, @@ -73,7 +74,8 @@ async fn main() -> Result<(), DatabricksKubeError> { ensure_crd("databricksjobs.com.dstancu.databricks", crd_api.clone()).await?; ensure_crd("gitcredentials.com.dstancu.databricks", crd_api.clone()).await?; - ensure_crd("repos.com.dstancu.databricks", crd_api).await?; + ensure_crd("repos.com.dstancu.databricks", crd_api.clone()).await?; + ensure_crd("databrickssecretscopes.com.dstancu.databricks", crd_api).await?; ensure_configmap(cm_api.clone()).await?; let configmap_store = watch_configmap(cm_api.clone()).await?; @@ -95,6 +97,7 @@ async fn main() -> Result<(), DatabricksKubeError> { let git_credential_controller = GitCredential::controller(ctx.clone()); let repo_controller = Repo::controller(ctx.clone()); + let secret_scope_controller = DatabricksSecretScope::controller(ctx.clone()); Toplevel::new() .start( @@ -137,6 +140,17 @@ async fn main() -> Result<(), DatabricksKubeError> { }) }, ) + .start( + "secret_scope_controller", + |_: SubsystemHandle| { + secret_scope_controller + .for_each(log_controller_event) + .map(|_| { + let res: Result<(), DatabricksKubeError> = Ok(()); + res + }) + }, + ) .catch_signals() .handle_shutdown_requests(Duration::from_secs(1)) .await diff --git a/databricks-kube/src/traits/rest_config.rs b/databricks-kube/src/traits/rest_config.rs index 3e75ed8..4701a73 100644 --- a/databricks-kube/src/traits/rest_config.rs +++ b/databricks-kube/src/traits/rest_config.rs @@ -12,6 +12,12 @@ use databricks_rust_repos::{ apis::configuration::Configuration as RepoClientConfig, models::GetRepoResponse as Repo, }; +use databricks_rust_secrets::{ + apis::configuration::Configuration as SecretClientConfig, + models::WorkspaceGetSecretResponse as Secret, models::WorkspaceListScopesResponse as Scopes, + models::WorkspaceSecretScope as Scope, +}; + use futures::FutureExt; use tokio::time::interval; @@ -21,84 +27,43 @@ pub trait RestConfig { ) -> Pin> + std::marker::Send>>; } -impl RestConfig for Job { - fn get_rest_config( - context: Arc, - ) -> Pin> + std::marker::Send>> { - let mut wait = interval(Duration::from_secs(15)); +#[macro_export] +macro_rules! openapi_config_glue { + ($client_config:ident, $dto:ident) => { + impl RestConfig<$client_config> for $dto { + fn get_rest_config( + context: Arc, + ) -> Pin> + std::marker::Send>> + { + let mut wait = interval(Duration::from_secs(15)); - async move { - while let None = context.get_api_secret() { - wait.tick().await; - log::info!("Waiting for REST credentials..."); - } + async move { + while let None = context.get_api_secret() { + wait.tick().await; + log::info!("Waiting for REST credentials..."); + } - let api_secret = context.get_api_secret()?; - let (url, token) = ( - api_secret.databricks_url.unwrap().to_string(), - api_secret.access_token.unwrap().to_string(), - ); - Some(JobClientConfig { - base_path: url, - bearer_access_token: Some(token), - ..JobClientConfig::default() - }) - } - .boxed() - } -} - -impl RestConfig for GitCredential { - fn get_rest_config( - context: Arc, - ) -> Pin> + std::marker::Send>> - { - let mut wait = interval(Duration::from_secs(15)); + let api_secret = context.get_api_secret()?; + let (url, token) = ( + api_secret.databricks_url.unwrap().to_string(), + api_secret.access_token.unwrap().to_string(), + ); - async move { - while let None = context.get_api_secret() { - wait.tick().await; - log::info!("Waiting for REST credentials..."); + Some($client_config { + base_path: url, + bearer_access_token: Some(token), + ..$client_config::default() + }) + } + .boxed() } - - let api_secret = context.get_api_secret()?; - let (url, token) = ( - api_secret.databricks_url.unwrap().to_string(), - api_secret.access_token.unwrap().to_string(), - ); - Some(GitCredentialClientConfig { - base_path: format!("{}/2.0", url), - bearer_access_token: Some(token), - ..GitCredentialClientConfig::default() - }) } - .boxed() - } + }; } -impl RestConfig for Repo { - fn get_rest_config( - context: Arc, - ) -> Pin> + std::marker::Send>> { - let mut wait = interval(Duration::from_secs(15)); - - async move { - while let None = context.get_api_secret() { - wait.tick().await; - log::info!("Waiting for REST credentials..."); - } - - let api_secret = context.get_api_secret()?; - let (url, token) = ( - api_secret.databricks_url.unwrap().to_string(), - api_secret.access_token.unwrap().to_string(), - ); - Some(RepoClientConfig { - base_path: format!("{}/2.0", url), - bearer_access_token: Some(token), - ..RepoClientConfig::default() - }) - } - .boxed() - } -} +openapi_config_glue!(JobClientConfig, Job); +openapi_config_glue!(GitCredentialClientConfig, GitCredential); +openapi_config_glue!(RepoClientConfig, Repo); +openapi_config_glue!(SecretClientConfig, Secret); +openapi_config_glue!(SecretClientConfig, Scope); +openapi_config_glue!(SecretClientConfig, Scopes); diff --git a/databricks-kube/tests/common/mock_k8s.rs b/databricks-kube/tests/common/mock_k8s.rs index d185bc9..bb87413 100644 --- a/databricks-kube/tests/common/mock_k8s.rs +++ b/databricks-kube/tests/common/mock_k8s.rs @@ -1,6 +1,5 @@ #![allow(dead_code)] -use std::time::{self, SystemTime}; use crate::common::fake_resource::FakeResource; diff --git a/databricks-rust-secrets/src/apis/secret_api.rs b/databricks-rust-secrets/src/apis/secret_api.rs index 512ddb0..3f434fe 100644 --- a/databricks-rust-secrets/src/apis/secret_api.rs +++ b/databricks-rust-secrets/src/apis/secret_api.rs @@ -99,12 +99,15 @@ pub async fn create_scope(configuration: &configuration::Configuration, workspac let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/scopes/create", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/scopes/create", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; local_var_req_builder = local_var_req_builder.json(&workspace_create_scope); let local_var_req = local_var_req_builder.build()?; @@ -128,12 +131,15 @@ pub async fn delete_acl(configuration: &configuration::Configuration, workspace_ let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/acls/delete", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/acls/delete", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; local_var_req_builder = local_var_req_builder.json(&workspace_delete_acl); let local_var_req = local_var_req_builder.build()?; @@ -157,12 +163,15 @@ pub async fn delete_scope(configuration: &configuration::Configuration, workspac let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/scopes/delete", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/scopes/delete", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; local_var_req_builder = local_var_req_builder.json(&workspace_delete_scope); let local_var_req = local_var_req_builder.build()?; @@ -186,12 +195,15 @@ pub async fn delete_secret(configuration: &configuration::Configuration, workspa let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/delete", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/delete", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; local_var_req_builder = local_var_req_builder.json(&workspace_delete_secret); let local_var_req = local_var_req_builder.build()?; @@ -215,7 +227,7 @@ pub async fn get_acl(configuration: &configuration::Configuration, scope: &str, let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/acls/get", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/acls/get", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); local_var_req_builder = local_var_req_builder.query(&[("scope", &scope.to_string())]); @@ -223,6 +235,9 @@ pub async fn get_acl(configuration: &configuration::Configuration, scope: &str, if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; let local_var_req = local_var_req_builder.build()?; let local_var_resp = local_var_client.execute(local_var_req).await?; @@ -245,7 +260,7 @@ pub async fn get_secret(configuration: &configuration::Configuration, scope: &st let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/get", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/get", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); local_var_req_builder = local_var_req_builder.query(&[("scope", &scope.to_string())]); @@ -253,6 +268,9 @@ pub async fn get_secret(configuration: &configuration::Configuration, scope: &st if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; let local_var_req = local_var_req_builder.build()?; let local_var_resp = local_var_client.execute(local_var_req).await?; @@ -275,13 +293,16 @@ pub async fn list_acls(configuration: &configuration::Configuration, scope: &str let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/acls/list", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/acls/list", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); local_var_req_builder = local_var_req_builder.query(&[("scope", &scope.to_string())]); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; let local_var_req = local_var_req_builder.build()?; let local_var_resp = local_var_client.execute(local_var_req).await?; @@ -304,12 +325,15 @@ pub async fn list_scopes(configuration: &configuration::Configuration, ) -> Resu let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/scopes/list", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/scopes/list", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; let local_var_req = local_var_req_builder.build()?; let local_var_resp = local_var_client.execute(local_var_req).await?; @@ -332,13 +356,16 @@ pub async fn list_secrets(configuration: &configuration::Configuration, scope: & let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/list", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/list", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); local_var_req_builder = local_var_req_builder.query(&[("scope", &scope.to_string())]); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; let local_var_req = local_var_req_builder.build()?; let local_var_resp = local_var_client.execute(local_var_req).await?; @@ -361,12 +388,15 @@ pub async fn put_acl(configuration: &configuration::Configuration, workspace_put let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/acls/put", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/acls/put", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; local_var_req_builder = local_var_req_builder.json(&workspace_put_acl); let local_var_req = local_var_req_builder.build()?; @@ -390,12 +420,15 @@ pub async fn put_secret(configuration: &configuration::Configuration, workspace_ let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/2.0/secrets/put", local_var_configuration.base_path); + let local_var_uri_str = format!("{}/2.0/secrets/put", local_var_configuration.base_path); let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } + if let Some(ref local_var_token) = local_var_configuration.bearer_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; local_var_req_builder = local_var_req_builder.json(&workspace_put_secret); let local_var_req = local_var_req_builder.build()?; diff --git a/databricks-rust-secrets/src/models/workspace_scope_backend_type.rs b/databricks-rust-secrets/src/models/workspace_scope_backend_type.rs index 9027083..2aeb315 100644 --- a/databricks-rust-secrets/src/models/workspace_scope_backend_type.rs +++ b/databricks-rust-secrets/src/models/workspace_scope_backend_type.rs @@ -36,4 +36,3 @@ impl Default for WorkspaceScopeBackendType { Self::Databricks } } - diff --git a/examples/databricks-secret-scope.yaml b/examples/databricks-secret-scope.yaml new file mode 100644 index 0000000..c82c026 --- /dev/null +++ b/examples/databricks-secret-scope.yaml @@ -0,0 +1,8 @@ +apiVersion: com.dstancu.databricks/v1 +kind: DatabricksSecretScope +metadata: + name: my-secret-scope + namespace: default +spec: + scope: + name: my-super-cool-scope \ No newline at end of file