do stuff (supposedly)

This commit is contained in:
nora 2024-04-12 21:15:24 +02:00
parent f9da7ebe43
commit c4e62f9d2d
12 changed files with 437 additions and 79 deletions

View file

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

76
src/example.rs Normal file
View file

@ -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<Box<dyn DataSource>> {
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<Value> {
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())),
])))
}
}

View file

@ -1,14 +0,0 @@
#![allow(dead_code)]
pub trait DataSource {
fn schema(&self);
fn read(&self) -> DResult<()>;
}
pub struct Diagnostics {
}
pub type DResult<T> = Result<T, Diagnostics>;
fn _data_source_obj_safe(_: &dyn DataSource) {}

View file

@ -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<Value>;
fn erase(self) -> Box<dyn DataSource>
where
Self: Sized + 'static,
{
Box::new(self)
}
}
pub struct Schema {
pub description: String,
pub attributes: HashMap<String, Attribute>,
}
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)
}
}

12
src/framework/mod.rs Normal file
View file

@ -0,0 +1,12 @@
#![allow(dead_code)]
pub mod datasource;
pub mod provider;
use self::datasource::DataSource;
pub struct Diagnostics {
pub(crate) errors: Vec<String>,
}
pub type DResult<T> = Result<T, Diagnostics>;

View file

@ -0,0 +1,6 @@
use super::DataSource;
pub trait Provider: Send + Sync {
fn name(&self) -> String;
fn data_sources(&self) -> Vec<Box<dyn DataSource>>;
}

View file

@ -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")?;
}

91
src/server/convert.rs Normal file
View file

@ -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: "<placeholder, this is a bug in terustform>".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<tfplugin6::Diagnostic> {
self.errors
.into_iter()
.map(|err| tfplugin6::Diagnostic {
severity: tfplugin6::diagnostic::Severity::Error as _,
summary: err,
detail: "".to_owned(),
attribute: None,
})
.collect()
}
}

View file

@ -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<tfplugin6::get_metadata::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<tfplugin6::get_provider_schema::Request>,
) -> Result<Response<tfplugin6::get_provider_schema::Response>, 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<tfplugin6::validate_provider_config::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<tfplugin6::validate_resource_config::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<tfplugin6::validate_data_resource_config::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<tfplugin6::upgrade_resource_state::Request>,
) -> Result<Response<tfplugin6::upgrade_resource_state::Response>, 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<tfplugin6::configure_provider::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<tfplugin6::read_resource::Request>,
) -> Result<Response<tfplugin6::read_resource::Response>, 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<tfplugin6::plan_resource_change::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<tfplugin6::apply_resource_change::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<tfplugin6::import_resource_state::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<tfplugin6::read_data_source::Request>,
) -> Result<Response<tfplugin6::read_data_source::Response>, 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(),
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![],
}),
deferred: None,
diagnostics: 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<plugin::Empty>) -> Result<Response<plugin::Empty>> {
shutdown(&self.shutdown).await
}

89
src/server/mod.rs Normal file
View file

@ -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<ProviderState, Vec<tfplugin6::Diagnostic>>,
}
struct ProviderState {
data_sources: HashMap<String, Box<dyn DataSource>>,
}
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::<HashMap<String, tfplugin6::Schema>>();
Schemas {
resources,
data_sources,
diagnostics: vec![],
}
}
}
struct Schemas {
resources: HashMap<String, tfplugin6::Schema>,
data_sources: HashMap<String, tfplugin6::Schema>,
diagnostics: Vec<tfplugin6::Diagnostic>,
}

View file

@ -20,7 +20,7 @@ impl Type {
// this is very dumb and wrong
pub enum Value {
String(String),
Object(BTreeMap<String, Box<Value>>)
Object(BTreeMap<String, Value>),
}
impl Value {

View file

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