This commit is contained in:
nora 2024-04-16 19:33:36 +02:00
parent 854f7bb2bc
commit 85d10ed893
15 changed files with 328 additions and 160 deletions

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# terustform
Terraform/OpenTofu Providers written in Rust!
**This project is extremely experimental and does not work well.**

View file

@ -0,0 +1 @@
/.env

View file

@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
eyre = "0.6.12"
reqwest = { version = "0.12.3", default-features = false, features = ["charset", "http2", "rustls-tls"]}
reqwest = { version = "0.12.3", default-features = false, features = ["charset", "http2", "json", "rustls-tls"] }
terustform = { path = "../terustform" }
tokio = { version = "1.37.0", features = ["full"] }

View file

@ -1,3 +1,51 @@
pub struct _CorsClient {
client: reqwest::Client
use eyre::{Context, OptionExt, Result};
use reqwest::header::{HeaderMap, HeaderValue};
#[derive(Clone)]
pub struct CorsClient {
client: reqwest::Client,
}
const URL: &str = "https://api.cors-school.nilstrieb.dev/api";
impl CorsClient {
pub async fn new(email: String, password: String) -> Result<Self> {
let client = reqwest::Client::new();
let login = dto::UserLogin { email, password };
let token = client
.post(format!("{URL}/login"))
.json(&login)
.send()
.await
.wrap_err("failed to send login request")?;
let token = token.error_for_status().wrap_err("failed to login")?;
let token = token
.headers()
.get("Token")
.ok_or_eyre("does not have Token header in login response")?
.to_str()
.wrap_err("Token is invalid utf8")?;
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {}", token,)).unwrap(),
);
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap();
Ok(Self { client })
}
pub async fn get_hugo(&self) -> Result<String> {
Ok(self
.client
.get(format!("{URL}/hugo"))
.send()
.await?
.text()
.await?)
}
}

View file

@ -1,11 +1,13 @@
mod client;
mod resources;
use std::collections::HashMap;
use eyre::Context;
use terustform::{
datasource::{self, DataSource},
datasource::DataSource,
provider::{MkDataSource, Provider},
AttrPath, DResult, StringValue, Value, ValueModel,
DResult, EyreExt, Schema, Value,
};
#[tokio::main]
@ -16,90 +18,36 @@ async fn main() -> eyre::Result<()> {
pub struct ExampleProvider {}
impl Provider for ExampleProvider {
type Data = ();
type Data = client::CorsClient;
fn name(&self) -> String {
"corsschool".to_owned()
}
fn schema(&self) -> datasource::Schema {
datasource::Schema {
fn schema(&self) -> Schema {
Schema {
description: "uwu".to_owned(),
attributes: HashMap::new(),
}
}
async fn configure(&self, _config: Value) -> DResult<Self::Data> {
Ok(())
let username = std::env::var("CORSSCHOOL_USERNAME")
.wrap_err("CORSSCHOOL_USERNAME environment variable not set")
.eyre_to_tf()?;
let password = std::env::var("CORSSCHOOL_PASSWORD")
.wrap_err("CORSSCHOOL_PASSWORD environment variable not set")
.eyre_to_tf()?;
let client = client::CorsClient::new(username, password)
.await
.wrap_err("failed to create client")
.eyre_to_tf()?;
Ok(client)
}
fn data_sources(&self) -> Vec<MkDataSource<Self::Data>> {
vec![ExampleDataSource::erase()]
}
}
struct ExampleDataSource {}
#[derive(terustform::Model)]
struct ExampleDataSourceModel {
name: StringValue,
meow: StringValue,
id: StringValue,
}
#[terustform::async_trait]
impl DataSource for ExampleDataSource {
type ProviderData = ();
fn name(provider_name: &str) -> String {
format!("{provider_name}_kitty")
}
fn schema() -> 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 new(_data: Self::ProviderData) -> DResult<Self> {
Ok(ExampleDataSource {})
}
async fn read(&self, config: Value) -> DResult<Value> {
let mut model = ExampleDataSourceModel::from_value(config, &AttrPath::root())?;
let name_str = model.name.expect_known(AttrPath::attr("name"))?;
let meow = format!("mrrrrr i am {name_str}");
model.meow = StringValue::Known(meow);
model.id = StringValue::Known("0".to_owned());
Ok(model.to_value())
vec![
resources::kitty::ExampleDataSource::erase(),
resources::hugo::HugoDataSource::erase(),
]
}
}

View file

@ -0,0 +1,68 @@
use std::collections::HashMap;
use eyre::Context;
use terustform::{
datasource::DataSource, Attribute, DResult, EyreExt, Mode, Schema, StringValue, Value,
ValueModel,
};
use crate::client::CorsClient;
pub struct HugoDataSource {
client: CorsClient,
}
#[derive(terustform::Model)]
struct HugoDataSourceModel {
hugo: StringValue,
}
#[terustform::async_trait]
impl DataSource for HugoDataSource {
type ProviderData = CorsClient;
async fn read(&self, _config: Value) -> DResult<Value> {
let hugo = self
.client
.get_hugo()
.await
.wrap_err("failed to get hugo")
.eyre_to_tf()?;
Ok(HugoDataSourceModel {
hugo: StringValue::Known(hugo),
}
.to_value())
}
fn name(provider_name: &str) -> String
where
Self: Sized,
{
format!("{provider_name}_hugo")
}
fn schema() -> Schema
where
Self: Sized,
{
Schema {
description: "Get Hugo Boss".to_owned(),
attributes: HashMap::from([(
"hugo".to_owned(),
Attribute::String {
description: "Hugo Boss".to_owned(),
mode: Mode::Computed,
sensitive: false,
},
)]),
}
}
fn new(data: Self::ProviderData) -> DResult<Self>
where
Self: Sized,
{
Ok(Self { client: data })
}
}

View file

@ -0,0 +1,75 @@
use std::collections::HashMap;
use terustform::{
datasource::DataSource, AttrPath, Attribute, DResult, Mode, Schema, StringValue, Value,
ValueModel,
};
use crate::client::CorsClient;
pub struct ExampleDataSource {}
#[derive(terustform::Model)]
struct ExampleDataSourceModel {
name: StringValue,
meow: StringValue,
id: StringValue,
}
#[terustform::async_trait]
impl DataSource for ExampleDataSource {
type ProviderData = CorsClient;
fn name(provider_name: &str) -> String {
format!("{provider_name}_kitty")
}
fn schema() -> Schema {
Schema {
description: "an example".to_owned(),
attributes: HashMap::from([
(
"name".to_owned(),
Attribute::String {
description: "a cool name".to_owned(),
mode: Mode::Required,
sensitive: false,
},
),
(
"meow".to_owned(),
Attribute::String {
description: "the meow of the cat".to_owned(),
mode: Mode::Computed,
sensitive: false,
},
),
(
"id".to_owned(),
Attribute::String {
description: "the ID of the meowy cat".to_owned(),
mode: Mode::Computed,
sensitive: false,
},
),
]),
}
}
fn new(_data: Self::ProviderData) -> DResult<Self> {
Ok(ExampleDataSource {})
}
async fn read(&self, config: Value) -> DResult<Value> {
let mut model = ExampleDataSourceModel::from_value(config, &AttrPath::root())?;
let name_str = model.name.expect_known(AttrPath::attr("name"))?;
let meow = format!("mrrrrr i am {name_str}");
model.meow = StringValue::Known(meow);
model.id = StringValue::Known("0".to_owned());
Ok(model.to_value())
}
}

View file

@ -0,0 +1,2 @@
pub mod hugo;
pub mod kitty;

View file

@ -22,3 +22,8 @@ output "cat1" {
output "cat2" {
value = data.corsschool_kitty.hellyes.meow
}
data "corsschool_hugo" "hugo" {}
output "hugo" {
value = data.corsschool_hugo.hugo
}

View file

@ -1,8 +1,7 @@
use std::collections::HashMap;
use crate::{
provider::{MkDataSource, ProviderData},
values::{Type, Value},
values::Value,
Schema,
};
use super::DResult;
@ -31,67 +30,3 @@ pub trait DataSource: Send + Sync + 'static {
MkDataSource::create::<Self>()
}
}
#[derive(Clone)]
pub struct Schema {
pub description: String,
pub attributes: HashMap<String, Attribute>,
}
#[derive(Clone)]
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)
}
}
impl Schema {
pub fn typ(&self) -> Type {
let attrs = self
.attributes
.iter()
.map(|(name, attr)| {
let attr_type = match attr {
Attribute::Int64 { .. } => Type::Number,
Attribute::String { .. } => Type::String,
};
(name.clone(), attr_type)
})
.collect();
Type::Object {
attrs,
optionals: vec![],
}
}
}

View file

@ -60,6 +60,16 @@ impl AttrPath {
}
}
pub trait EyreExt<T> {
fn eyre_to_tf(self) -> DResult<T>;
}
impl<T> EyreExt<T> for Result<T, eyre::Report> {
fn eyre_to_tf(self) -> DResult<T> {
self.map_err(|e| Diagnostic::error_string(format!("{:?}", e)).into())
}
}
impl<E: std::error::Error + std::fmt::Debug> From<E> for Diagnostic {
fn from(value: E) -> Self {
Self::error_string(format!("{:?}", value))

View file

@ -1,11 +1,18 @@
mod diag;
// Internal modules
mod server;
// Modules re-exported in the root
mod diag;
mod schema;
mod values;
// Public modules
pub mod datasource;
pub mod provider;
// Re-exports
pub use diag::*;
pub use schema::*;
pub use values::*;
pub use terustform_macros::Model;
@ -13,6 +20,9 @@ pub use terustform_macros::Model;
pub use async_trait::async_trait;
pub use eyre;
// --------
// Rest of the file.
use provider::Provider;
use tracing::Level;

View file

@ -1,22 +1,19 @@
use std::{future::Future, sync::Arc};
use crate::{
datasource::{self, DataSource, Schema},
DResult, Value,
};
use crate::{datasource::DataSource, DResult, Schema, Value};
pub trait ProviderData: Clone + Send + Sync + 'static {}
impl<D: Clone + Send + Sync + 'static> ProviderData for D {}
pub struct MkDataSource<D: ProviderData> {
pub(crate) name: fn(&str) -> String,
pub(crate) schema: datasource::Schema,
pub(crate) schema: Schema,
pub(crate) mk: fn(D) -> DResult<StoredDataSource<D>>,
}
pub(crate) struct StoredDataSource<D: ProviderData> {
pub(crate) ds: Arc<dyn DataSource<ProviderData = D>>,
pub(crate) schema: datasource::Schema,
pub(crate) schema: Schema,
}
impl<D: ProviderData> Clone for StoredDataSource<D> {

68
terustform/src/schema.rs Normal file
View file

@ -0,0 +1,68 @@
use std::collections::HashMap;
use crate::Type;
#[derive(Clone)]
pub struct Schema {
pub description: String,
pub attributes: HashMap<String, Attribute>,
}
#[derive(Clone)]
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)
}
}
impl Schema {
pub fn typ(&self) -> Type {
let attrs = self
.attributes
.iter()
.map(|(name, attr)| {
let attr_type = match attr {
Attribute::Int64 { .. } => Type::Number,
Attribute::String { .. } => Type::String,
};
(name.clone(), attr_type)
})
.collect();
Type::Object {
attrs,
optionals: vec![],
}
}
}

View file

@ -1,12 +1,8 @@
use crate::{
datasource::{self, Mode},
values::Type,
AttrPathSegment, Diagnostics,
};
use crate::{values::Type, AttrPathSegment, Attribute, Diagnostics, Mode, Schema};
use super::grpc::tfplugin6;
impl datasource::Schema {
impl Schema {
pub(crate) fn to_tfplugin(self) -> tfplugin6::Schema {
tfplugin6::Schema {
version: 1,
@ -26,7 +22,7 @@ impl datasource::Schema {
}
}
impl datasource::Attribute {
impl Attribute {
pub(crate) fn to_tfplugin(self, name: String) -> tfplugin6::schema::Attribute {
let mut attr = tfplugin6::schema::Attribute {
name,
@ -48,7 +44,7 @@ impl datasource::Attribute {
};
match self {
datasource::Attribute::String {
Attribute::String {
description,
mode,
sensitive,
@ -58,7 +54,7 @@ impl datasource::Attribute {
set_modes(&mut attr, mode);
attr.sensitive = sensitive;
}
datasource::Attribute::Int64 {
Attribute::Int64 {
description,
mode,
sensitive,