From c4e62f9d2dcddae9c6cb0634556b4b95a3d02597 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Fri, 12 Apr 2024 21:15:24 +0200 Subject: [PATCH] do stuff (supposedly) --- Cargo.toml | 4 +- src/example.rs | 76 ++++++++++++++++++ src/framework.rs | 14 ---- src/framework/datasource.rs | 59 ++++++++++++++ src/framework/mod.rs | 12 +++ src/framework/provider.rs | 6 ++ src/main.rs | 24 +++--- src/server/convert.rs | 91 +++++++++++++++++++++ src/{server.rs => server/grpc.rs} | 126 ++++++++++++++++++------------ src/server/mod.rs | 89 +++++++++++++++++++++ src/values.rs | 2 +- test/main.tf | 13 ++- 12 files changed, 437 insertions(+), 79 deletions(-) create mode 100644 src/example.rs delete mode 100644 src/framework.rs create mode 100644 src/framework/datasource.rs create mode 100644 src/framework/mod.rs create mode 100644 src/framework/provider.rs create mode 100644 src/server/convert.rs rename src/{server.rs => server/grpc.rs} (72%) create mode 100644 src/server/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 79111b8..3c736ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,10 @@ name = "terraform-provider-terustform" version = "0.1.0" edition = "2021" +[profile.release] +panic = "abort" + [profile.dev] -opt-level = 1 panic = "abort" [dependencies] diff --git a/src/example.rs b/src/example.rs new file mode 100644 index 0000000..a44922b --- /dev/null +++ b/src/example.rs @@ -0,0 +1,76 @@ +use std::collections::{BTreeMap, HashMap}; + +use crate::{ + framework::{ + datasource::{self, DataSource}, + provider::Provider, + DResult, + }, + values::Value, +}; + +pub struct ExampleProvider {} + +impl Provider for ExampleProvider { + fn name(&self) -> String { + "terustform".to_owned() + } + + fn data_sources(&self) -> Vec> { + vec![ExampleDataSource {}.erase()] + } +} + +struct ExampleDataSource {} + +impl DataSource for ExampleDataSource { + fn name(&self, provider_name: &str) -> String { + format!("{provider_name}_kitty") + } + + fn schema(&self) -> datasource::Schema { + datasource::Schema { + description: "an example".to_owned(), + attributes: HashMap::from([ + ( + "name".to_owned(), + datasource::Attribute::String { + description: "a cool name".to_owned(), + mode: datasource::Mode::Required, + sensitive: false, + }, + ), + ( + "meow".to_owned(), + datasource::Attribute::String { + description: "the meow of the cat".to_owned(), + mode: datasource::Mode::Computed, + sensitive: false, + }, + ), + ( + "id".to_owned(), + datasource::Attribute::String { + description: "the ID of the meowy cat".to_owned(), + mode: datasource::Mode::Computed, + sensitive: false, + }, + ), + ]), + } + } + + fn read(&self, config: Value) -> DResult { + Ok(Value::Object(BTreeMap::from([ + ( + "name".to_owned(), + match config { + Value::Object(mut obj) => obj.remove("name").unwrap(), + _ => unreachable!(), + }, + ), + ("meow".to_owned(), Value::String("mrrrrr".to_owned())), + ("id".to_owned(), Value::String("0".to_owned())), + ]))) + } +} diff --git a/src/framework.rs b/src/framework.rs deleted file mode 100644 index 32f41e0..0000000 --- a/src/framework.rs +++ /dev/null @@ -1,14 +0,0 @@ -#![allow(dead_code)] - -pub trait DataSource { - fn schema(&self); - fn read(&self) -> DResult<()>; -} - -pub struct Diagnostics { - -} - -pub type DResult = Result; - -fn _data_source_obj_safe(_: &dyn DataSource) {} diff --git a/src/framework/datasource.rs b/src/framework/datasource.rs new file mode 100644 index 0000000..798497a --- /dev/null +++ b/src/framework/datasource.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; + +use crate::values::Value; + +use super::DResult; + +pub trait DataSource: Send + Sync { + fn name(&self, provider_name: &str) -> String; + fn schema(&self) -> Schema; + // todo: probably want some kind of Value+Schema thing like tfsdk? whatever. + fn read(&self, config: Value) -> DResult; + + fn erase(self) -> Box + where + Self: Sized + 'static, + { + Box::new(self) + } +} + +pub struct Schema { + pub description: String, + pub attributes: HashMap, +} + +pub enum Attribute { + String { + description: String, + mode: Mode, + sensitive: bool, + }, + Int64 { + description: String, + mode: Mode, + sensitive: bool, + }, +} + +#[derive(Clone, Copy)] +pub enum Mode { + Required, + Optional, + OptionalComputed, + Computed, +} + +impl Mode { + pub fn required(&self) -> bool { + matches!(self, Self::Required) + } + + pub fn optional(&self) -> bool { + matches!(self, Self::Optional | Self::OptionalComputed) + } + + pub fn computed(&self) -> bool { + matches!(self, Self::OptionalComputed | Self::Computed) + } +} \ No newline at end of file diff --git a/src/framework/mod.rs b/src/framework/mod.rs new file mode 100644 index 0000000..425f5e9 --- /dev/null +++ b/src/framework/mod.rs @@ -0,0 +1,12 @@ +#![allow(dead_code)] + +pub mod datasource; +pub mod provider; + +use self::datasource::DataSource; + +pub struct Diagnostics { + pub(crate) errors: Vec, +} + +pub type DResult = Result; diff --git a/src/framework/provider.rs b/src/framework/provider.rs new file mode 100644 index 0000000..e6e2f86 --- /dev/null +++ b/src/framework/provider.rs @@ -0,0 +1,6 @@ +use super::DataSource; + +pub trait Provider: Send + Sync { + fn name(&self) -> String; + fn data_sources(&self) -> Vec>; +} diff --git a/src/main.rs b/src/main.rs index dee01a9..9f3d551 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,19 +2,25 @@ mod cert; mod framework; mod server; mod values; +mod example; use std::{env, path::PathBuf}; use base64::Engine; use eyre::{bail, Context, Result}; +use framework::provider::Provider; use tokio::net::UnixListener; use tonic::transport::{Certificate, ServerTlsConfig}; use tracing::{info, Level}; #[tokio::main] async fn main() -> eyre::Result<()> { + serve(&example::ExampleProvider {}).await +} + +async fn serve(provider: &dyn Provider) -> eyre::Result<()> { tracing_subscriber::fmt() - .with_max_level(Level::DEBUG) + .with_max_level(Level::ERROR) .with_writer(std::io::stderr) .without_time() .init(); @@ -43,27 +49,25 @@ async fn main() -> eyre::Result<()> { let uds = UnixListener::bind(socket).wrap_err("failed to bind unix listener")?; let uds_stream = tokio_stream::wrappers::UnixListenerStream::new(uds); - let token = tokio_util::sync::CancellationToken::new(); + let shutdown = tokio_util::sync::CancellationToken::new(); let server = tonic::transport::Server::builder() .tls_config(tls) .wrap_err("invalid TLS config")? - .add_service(server::tfplugin6::provider_server::ProviderServer::new( - server::MyProvider { - shutdown: token.clone(), - }, + .add_service(server::ProviderServer::new( + server::ProviderHandler::new(shutdown.clone(), provider), )) .add_service( - server::plugin::grpc_controller_server::GrpcControllerServer::new( - server::MyController { - shutdown: token.clone(), + server::GrpcControllerServer::new( + server::Controller { + shutdown: shutdown.clone(), }, ), ) .serve_with_incoming(uds_stream); tokio::select! { - _ = token.cancelled() => {} + _ = shutdown.cancelled() => {} result = server => { result.wrap_err("failed to start server")?; } diff --git a/src/server/convert.rs b/src/server/convert.rs new file mode 100644 index 0000000..1ee4e8f --- /dev/null +++ b/src/server/convert.rs @@ -0,0 +1,91 @@ +use crate::{ + framework::{ + datasource::{self, Mode}, + Diagnostics, + }, + values::Type, +}; + +use super::grpc::tfplugin6; + +impl datasource::Schema { + pub(crate) fn to_tfplugin(self) -> tfplugin6::Schema { + tfplugin6::Schema { + version: 1, + block: Some(tfplugin6::schema::Block { + version: 0, + attributes: self + .attributes + .into_iter() + .map(|(name, attr)| attr.to_tfplugin(name)) + .collect(), + block_types: vec![], + description: self.description, + description_kind: tfplugin6::StringKind::Markdown as _, + deprecated: false, + }), + } + } +} + +impl datasource::Attribute { + pub(crate) fn to_tfplugin(self, name: String) -> tfplugin6::schema::Attribute { + let mut attr = tfplugin6::schema::Attribute { + name, + r#type: vec![], + nested_type: None, + description: "".to_owned(), + required: false, + optional: false, + computed: true, + sensitive: false, + description_kind: tfplugin6::StringKind::Markdown as _, + deprecated: false, + }; + + let set_modes = |attr: &mut tfplugin6::schema::Attribute, mode: Mode| { + attr.required = mode.required(); + attr.optional = mode.optional(); + attr.computed = mode.computed(); + }; + + match self { + datasource::Attribute::String { + description, + mode, + sensitive, + } => { + attr.r#type = Type::String.to_json().into_bytes(); + attr.description = description; + set_modes(&mut attr, mode); + attr.sensitive = sensitive; + } + datasource::Attribute::Int64 { + description, + mode, + sensitive, + } => { + attr.r#type = Type::Number.to_json().into_bytes(); + attr.description = description; + set_modes(&mut attr, mode); + attr.sensitive = sensitive; + } + } + + attr + } +} + +impl Diagnostics { + pub(crate) fn to_tfplugin_diags(self) -> Vec { + self.errors + .into_iter() + .map(|err| tfplugin6::Diagnostic { + severity: tfplugin6::diagnostic::Severity::Error as _, + summary: err, + detail: "".to_owned(), + attribute: None, + }) + .collect() + } +} diff --git a/src/server.rs b/src/server/grpc.rs similarity index 72% rename from src/server.rs rename to src/server/grpc.rs index b23ad7f..1fdec24 100644 --- a/src/server.rs +++ b/src/server/grpc.rs @@ -21,11 +21,6 @@ use tracing::info; use crate::values::Type; -#[derive(Debug)] -pub struct MyProvider { - pub shutdown: CancellationToken, -} - fn empty_schema() -> tfplugin6::Schema { tfplugin6::Schema { version: 1, @@ -41,12 +36,13 @@ fn empty_schema() -> tfplugin6::Schema { } #[tonic::async_trait] -impl Provider for MyProvider { +impl Provider for super::ProviderHandler { /// GetMetadata returns upfront information about server capabilities and /// supported resource types without requiring the server to instantiate all /// schema information, which may be memory intensive. This RPC is optional, /// where clients may receive an unimplemented RPC error. Clients should /// ignore the error and call the GetProviderSchema RPC as a fallback. + /// Returns data source, managed resource, and function metadata, such as names. async fn get_metadata( &self, request: Request, @@ -58,11 +54,15 @@ impl Provider for MyProvider { } /// GetSchema returns schema information for the provider, data resources, /// and managed resources. + /// Returns provider schema, provider metaschema, all resource schemas and all data source schemas. async fn get_provider_schema( &self, request: Request, ) -> Result, Status> { info!("Received get_provider_schema"); + + let schemas = self.get_schemas(); + let reply = tfplugin6::get_provider_schema::Response { provider: Some(empty_schema()), provider_meta: Some(empty_schema()), @@ -71,38 +71,16 @@ impl Provider for MyProvider { get_provider_schema_optional: true, move_resource_state: false, }), - data_source_schemas: HashMap::from([( - "terustform_kitty".to_owned(), - tfplugin6::Schema { - version: 1, - block: Some(tfplugin6::schema::Block { - version: 0, - attributes: vec![tfplugin6::schema::Attribute { - name: "kitten".to_owned(), - r#type: Type::String.to_json().into_bytes(), - nested_type: None, - description: "what sound does the kitten make?".to_owned(), - required: false, - optional: false, - computed: true, - sensitive: false, - description_kind: 0, - deprecated: false, - }], - block_types: vec![], - description: "something or nothing?".to_owned(), - description_kind: 0, - deprecated: false, - }), - }, - )]), - resource_schemas: HashMap::from([("terustform_hello".to_owned(), empty_schema())]), + data_source_schemas: schemas.data_sources, + resource_schemas: schemas.resources, functions: HashMap::default(), - diagnostics: vec![], + diagnostics: schemas.diagnostics, }; Ok(Response::new(reply)) } + + /// Validates the practitioner supplied provider configuration by verifying types conform to the schema and supports value validation diagnostics. async fn validate_provider_config( &self, request: Request, @@ -115,6 +93,8 @@ impl Provider for MyProvider { Ok(Response::new(reply)) } + + /// Validates the practitioner supplied resource configuration by verifying types conform to the schema and supports value validation diagnostics. async fn validate_resource_config( &self, request: Request, @@ -127,6 +107,8 @@ impl Provider for MyProvider { Ok(Response::new(reply)) } + + /// Validates the practitioner supplied data source configuration by verifying types conform to the schema and supports value validation diagnostics. async fn validate_data_resource_config( &self, request: Request, @@ -139,14 +121,24 @@ impl Provider for MyProvider { Ok(Response::new(reply)) } + + /// Called when a resource has existing state. Primarily useful for when the schema version does not match the current version. + /// The provider is expected to modify the state to upgrade it to the latest schema. async fn upgrade_resource_state( &self, request: Request, ) -> Result, Status> { - tracing::error!("upgrade_resource_state"); - todo!("upgrade_resource_state") + tracing::info!("upgrade_resource_state"); + // We don't do anything interesting, it's fine. + let reply = tfplugin6::upgrade_resource_state::Response { + upgraded_state: None, + diagnostics: vec![], + }; + + Ok(Response::new(reply)) } /// ////// One-time initialization, called before other functions below + /// Passes the practitioner supplied provider configuration to the provider. async fn configure_provider( &self, request: Request, @@ -158,13 +150,24 @@ impl Provider for MyProvider { Ok(Response::new(reply)) } /// ////// Managed Resource Lifecycle + /// Called when refreshing a resource's state. async fn read_resource( &self, request: Request, ) -> Result, Status> { - tracing::error!("read_resource"); - todo!("read_resource") + tracing::info!("read_resource"); + + let reply = tfplugin6::read_resource::Response { + deferred: None, + diagnostics: vec![], + new_state: request.into_inner().current_state, + private: vec![], + }; + + Ok(Response::new(reply)) } + + /// Calculates a plan for a resource. A proposed new state is generated, which the provider can modify. async fn plan_resource_change( &self, request: Request, @@ -182,6 +185,9 @@ impl Provider for MyProvider { Ok(Response::new(reply)) } + + /// Called when a practitioner has approved a planned change. + /// The provider is to apply the changes contained in the plan, and return a resulting state matching the given plan. async fn apply_resource_change( &self, request: Request, @@ -197,6 +203,8 @@ impl Provider for MyProvider { Ok(Response::new(reply)) } + + /// Called when importing a resource into state so that the resource becomes managed. async fn import_resource_state( &self, request: Request, @@ -213,26 +221,42 @@ impl Provider for MyProvider { todo!("move_resource_state") } + + /// Called when refreshing a data source's state. async fn read_data_source( &self, request: Request, ) -> Result, Status> { tracing::info!("read_data_source"); - let reply = tfplugin6::read_data_source::Response { - state: Some(tfplugin6::DynamicValue { - msgpack: crate::values::Value::Object(BTreeMap::from([( - "kitten".to_owned(), - Box::new(crate::values::Value::String("meow".to_owned())), - )])) - .msg_pack(), - json: vec![], - }), - deferred: None, - diagnostics: vec![], + let ds = self + .state + .as_ref() + .unwrap() + .data_sources + .get(&request.get_ref().type_name) + .unwrap(); + + let state = ds.read(crate::values::Value::Object(BTreeMap::from([( + "name".to_owned(), + crate::values::Value::String("mykitten".to_owned()), + )]))); + let (state, diagnostics) = match state { + Ok(s) => ( + Some(tfplugin6::DynamicValue { + msgpack: s.msg_pack(), + json: vec![], + }), + vec![], + ), + Err(errs) => (None, errs.to_tfplugin_diags()), }; - dbg!(request); + let reply = tfplugin6::read_data_source::Response { + state, + deferred: None, + diagnostics, + }; Ok(Response::new(reply)) } @@ -265,7 +289,7 @@ impl Provider for MyProvider { } } -pub struct MyController { +pub struct Controller { pub shutdown: CancellationToken, } @@ -276,7 +300,7 @@ async fn shutdown(token: &CancellationToken) -> ! { } #[tonic::async_trait] -impl plugin::grpc_controller_server::GrpcController for MyController { +impl plugin::grpc_controller_server::GrpcController for Controller { async fn shutdown(&self, request: Request) -> Result> { shutdown(&self.shutdown).await } diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..6112221 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,89 @@ +mod convert; +mod grpc; + +use std::collections::HashMap; + +use tokio_util::sync::CancellationToken; + +use crate::framework::datasource::{self, DataSource}; +use crate::framework::provider::Provider; +use crate::framework::DResult; + +pub use grpc::plugin::grpc_controller_server::GrpcControllerServer; +pub use grpc::tfplugin6::provider_server::ProviderServer; +pub use grpc::Controller; + +use self::grpc::tfplugin6; + +pub struct ProviderHandler { + shutdown: CancellationToken, + /// Delayed diagnostics reporting in `GetProviderSchema` for better UX. + state: Result>, +} + +struct ProviderState { + data_sources: HashMap>, +} + +impl ProviderHandler { + /// Creates a new `ProviderHandler`. + /// This function is infallible, as it is not called during a time where reporting errors nicely is possible. + /// If there's an error, we just taint our internal state and report errors in `GetProviderSchema`. + pub fn new(shutdown: CancellationToken, provider: &dyn Provider) -> Self { + let name = provider.name(); + let mut data_sources = HashMap::new(); + let mut errors = vec![]; + + for ds in provider.data_sources() { + let ds_name = ds.name(&name); + let entry = data_sources.insert(ds_name.clone(), ds); + if entry.is_some() { + errors.push(tfplugin6::Diagnostic { + severity: tfplugin6::diagnostic::Severity::Error as _, + summary: format!("data source {ds_name} exists more than once"), + detail: "".to_owned(), + attribute: None, + }); + } + } + + let state = if errors.len() > 0 { + Err(errors) + } else { + Ok(ProviderState { data_sources }) + }; + + Self { shutdown, state } + } + + fn get_schemas(&self) -> Schemas { + let resources = HashMap::new(); + let state = match &self.state { + Ok(state) => state, + Err(errors) => { + return Schemas { + resources: HashMap::new(), + data_sources: HashMap::new(), + diagnostics: errors.clone(), + } + } + }; + let data_sources = state + .data_sources + .iter() + .map(|(name, ds)| (name.to_owned(), ds.schema().to_tfplugin())) + .collect::>(); + + Schemas { + resources, + data_sources, + diagnostics: vec![], + } + } +} + +struct Schemas { + resources: HashMap, + data_sources: HashMap, + diagnostics: Vec, +} diff --git a/src/values.rs b/src/values.rs index 66cf99b..2e3f11f 100644 --- a/src/values.rs +++ b/src/values.rs @@ -20,7 +20,7 @@ impl Type { // this is very dumb and wrong pub enum Value { String(String), - Object(BTreeMap>) + Object(BTreeMap), } impl Value { diff --git a/test/main.tf b/test/main.tf index 368f3b3..f0a5859 100644 --- a/test/main.tf +++ b/test/main.tf @@ -10,8 +10,17 @@ provider "terustform" {} //resource "terustform_hello" "test1" {} -data "terustform_kitty" "kitty" {} +data "terustform_kitty" "kitty" { + name = "mykitten" +} + +data "terustform_kitty" "hellyes" { + name = "a cute kitty" +} output "meow" { - value = data.terustform_kitty.kitty.kitten + value = data.terustform_kitty.kitty.id +} +output "hellyes" { + value = data.terustform_kitty.kitty.meow } \ No newline at end of file