This commit is contained in:
nora 2024-04-10 20:57:16 +02:00
parent bac720c512
commit 69daf83c53
7 changed files with 442 additions and 91 deletions

29
src/cert.rs Normal file
View file

@ -0,0 +1,29 @@
use eyre::{Context, Result};
use rcgen::{DistinguishedName, DnType, IsCa, KeyUsagePurpose};
use time::Duration;
pub fn generate_cert() -> Result<(tonic::transport::Identity, rcgen::Certificate)> {
// https://github.com/hashicorp/go-plugin/blob/8d2aaa458971cba97c3bfec1b0380322e024b514/mtls.go#L20
let keypair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
.wrap_err("failed to generate keypair")?;
let mut params =
rcgen::CertificateParams::new(["localhost".to_owned()]).wrap_err("creating cert params")?;
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
KeyUsagePurpose::KeyAgreement,
KeyUsagePurpose::KeyCertSign,
];
params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.not_before = time::OffsetDateTime::now_utc().saturating_add(Duration::seconds(-30));
params.not_after = time::OffsetDateTime::now_utc().saturating_add(Duration::seconds(262980));
let mut dn = DistinguishedName::new();
dn.push(DnType::OrganizationName, "HashiCorp");
dn.push(DnType::CommonName, "localhost");
params.distinguished_name = dn;
let cert = params.self_signed(&keypair).wrap_err("signing cert")?;
Ok((tonic::transport::Identity::from_pem(cert.pem(), keypair.serialize_pem()), cert))
}

View file

@ -1,18 +1,29 @@
use std::{
env,
io::Write,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use eyre::{bail, ensure, Context, Result};
mod cert;
mod server;
use std::{env, path::PathBuf};
use base64::Engine;
use eyre::{bail, Context, Result};
use tokio::net::UnixListener;
use tonic::transport::{Certificate, ServerTlsConfig};
use tracing::{info, Level};
#[tokio::main]
async fn main() -> eyre::Result<()> {
let addr = init_handshake();
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.with_writer(std::io::stderr)
.without_time()
.init();
let addr = match addr {
let client_cert =
std::env::var("PLUGIN_CLIENT_CERT").wrap_err("PLUGIN_CLIENT_CERT not found")?;
let client_cert = Certificate::from_pem(client_cert);
let (server_identity, server_cert) =
cert::generate_cert().wrap_err("generating server certificate")?;
let (_tmpdir, socket) = match init_handshake(&server_cert).await {
Ok(addr) => addr,
Err(err) => {
println!("{:?}", err);
@ -20,40 +31,58 @@ async fn main() -> eyre::Result<()> {
}
};
let cert = std::env::var("PLUGIN_CLIENT_CERT").wrap_err("PLUGIN_CLIENT_CERT not found")?;
let tls = ServerTlsConfig::new()
.identity(server_identity)
.client_auth_optional(true) // ??? terraform doesn't send certs ???
.client_ca_root(client_cert);
info!("Listening on {}", socket.display());
let uds = UnixListener::bind(socket).wrap_err("failed to bind unix listener")?;
let uds_stream = tokio_stream::wrappers::UnixListenerStream::new(uds);
tonic::transport::Server::builder()
.tls_config(tls)
.wrap_err("invalid TLS config")?
.add_service(server::tfplugin6::provider_server::ProviderServer::new(
server::MyProvider,
))
.serve(addr)
.serve_with_incoming(uds_stream)
.await
.wrap_err("failed to start server")?;
Ok(())
}
fn init_handshake() -> Result<SocketAddr> {
const _MAGIC_COOKIE_KEY: &str = "TF_PLUGIN_MAGIC_COOKIE";
const _MAGIC_COOKIE_VALUE: &str =
"d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2";
async fn init_handshake(server_cert: &rcgen::Certificate) -> Result<(tempfile::TempDir, PathBuf)> {
// https://github.com/hashicorp/go-plugin/blob/8d2aaa458971cba97c3bfec1b0380322e024b514/docs/internals.md
let min_port = env::var("PLUGIN_MIN_PORT")
let _ = env::var("PLUGIN_MIN_PORT")
.wrap_err("PLUGIN_MIN_PORT not found")?
.parse::<u16>()
.wrap_err("PLUGIN_MIN_PORT not an int")?;
let max_port = env::var("PLUGIN_MAX_PORT")
let _ = env::var("PLUGIN_MAX_PORT")
.wrap_err("PLUGIN_MAX_PORT not found")?
.parse::<u16>()
.wrap_err("PLUGIN_MAX_PORT not an int")?;
let port = min_port + 15; // chosen by a d20, lol
ensure!(port < max_port);
let tmpdir = tempfile::TempDir::new().wrap_err("failed to create temporary directory")?;
let socket = tmpdir.path().join("plugin");
let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let addr = SocketAddr::new(addr, port);
// https://github.com/hashicorp/go-plugin/blob/8d2aaa458971cba97c3bfec1b0380322e024b514/server.go#L426
const CORE_PROTOCOL_VERSION: u8 = 1;
const PROTO_VERSION: u8 = 6;
let listener_addr_network = "unix";
let listener_addr = socket.display();
let proto_type = "grpc";
let b64_cert = base64::prelude::BASE64_STANDARD_NO_PAD.encode(server_cert.der());
const VERSION: u8 = 6;
println!("{CORE_PROTOCOL_VERSION}|{PROTO_VERSION}|{listener_addr_network}|{listener_addr}|{proto_type}|{b64_cert}");
println!("1|{VERSION}|tcp|{addr}|grpc");
Ok(addr)
Ok((tmpdir, socket))
}

View file

@ -1,13 +1,32 @@
#![allow(unused_variables, unused_imports)]
pub mod tfplugin6 {
tonic::include_proto!("tfplugin6");
}
use std::{collections::HashMap, vec};
use tfplugin6::provider_server::{Provider, ProviderServer};
use tonic::{transport::Server, Request, Response, Result, Status};
use tracing::info;
#[derive(Debug, Default)]
pub struct MyProvider;
fn empty_schema() -> tfplugin6::Schema {
tfplugin6::Schema {
version: 1,
block: Some(tfplugin6::schema::Block {
version: 0,
attributes: vec![],
block_types: vec![],
description: "hello world".to_owned(),
description_kind: 0,
deprecated: false,
}),
}
}
#[tonic::async_trait]
impl Provider for MyProvider {
/// GetMetadata returns upfront information about server capabilities and
@ -17,122 +36,166 @@ impl Provider for MyProvider {
/// ignore the error and call the GetProviderSchema RPC as a fallback.
async fn get_metadata(
&self,
request: tonic::Request<tfplugin6::get_metadata::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::get_metadata::Response>, tonic::Status> {
todo!()
request: Request<tfplugin6::get_metadata::Request>,
) -> Result<Response<tfplugin6::get_metadata::Response>, Status> {
Err(Status::unimplemented(
"GetMetadata: Not implemeneted".to_owned(),
))
}
/// GetSchema returns schema information for the provider, data resources,
/// and managed resources.
async fn get_provider_schema(
&self,
request: tonic::Request<tfplugin6::get_provider_schema::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::get_provider_schema::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::get_provider_schema::Request>,
) -> Result<Response<tfplugin6::get_provider_schema::Response>, Status> {
info!("Received get_provider_schema");
let reply = tfplugin6::get_provider_schema::Response {
provider: Some(empty_schema()),
provider_meta: Some(empty_schema()),
server_capabilities: Some(tfplugin6::ServerCapabilities {
plan_destroy: true,
get_provider_schema_optional: true,
move_resource_state: false,
}),
data_source_schemas: HashMap::default(),
resource_schemas: HashMap::from([("terustform_hello".to_owned(), empty_schema())]),
functions: HashMap::default(),
diagnostics: vec![],
};
Ok(Response::new(reply))
}
async fn validate_provider_config(
&self,
request: tonic::Request<tfplugin6::validate_provider_config::Request>,
) -> std::result::Result<
tonic::Response<tfplugin6::validate_provider_config::Response>,
tonic::Status,
> {
todo!()
request: Request<tfplugin6::validate_provider_config::Request>,
) -> Result<Response<tfplugin6::validate_provider_config::Response>, Status> {
tracing::info!("validate_provider_config");
let reply = tfplugin6::validate_provider_config::Response {
diagnostics: vec![],
};
Ok(Response::new(reply))
}
async fn validate_resource_config(
&self,
request: tonic::Request<tfplugin6::validate_resource_config::Request>,
) -> std::result::Result<
tonic::Response<tfplugin6::validate_resource_config::Response>,
tonic::Status,
> {
todo!()
request: Request<tfplugin6::validate_resource_config::Request>,
) -> Result<Response<tfplugin6::validate_resource_config::Response>, Status> {
tracing::info!("validate_resource_config");
let reply = tfplugin6::validate_resource_config::Response {
diagnostics: vec![],
};
Ok(Response::new(reply))
}
async fn validate_data_resource_config(
&self,
request: tonic::Request<tfplugin6::validate_data_resource_config::Request>,
) -> std::result::Result<
tonic::Response<tfplugin6::validate_data_resource_config::Response>,
tonic::Status,
> {
todo!()
request: Request<tfplugin6::validate_data_resource_config::Request>,
) -> Result<Response<tfplugin6::validate_data_resource_config::Response>, Status> {
tracing::error!("validate_data_resource_config");
todo!("validate_data_resource_config")
}
async fn upgrade_resource_state(
&self,
request: tonic::Request<tfplugin6::upgrade_resource_state::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::upgrade_resource_state::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::upgrade_resource_state::Request>,
) -> Result<Response<tfplugin6::upgrade_resource_state::Response>, Status> {
tracing::error!("upgrade_resource_state");
todo!("upgrade_resource_state")
}
/// ////// One-time initialization, called before other functions below
async fn configure_provider(
&self,
request: tonic::Request<tfplugin6::configure_provider::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::configure_provider::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::configure_provider::Request>,
) -> Result<Response<tfplugin6::configure_provider::Response>, Status> {
tracing::info!("configure_provider");
let reply = tfplugin6::configure_provider::Response {
diagnostics: vec![],
};
Ok(Response::new(reply))
}
/// ////// Managed Resource Lifecycle
async fn read_resource(
&self,
request: tonic::Request<tfplugin6::read_resource::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::read_resource::Response>, tonic::Status> {
todo!()
request: Request<tfplugin6::read_resource::Request>,
) -> Result<Response<tfplugin6::read_resource::Response>, Status> {
tracing::error!("read_resource");
todo!("read_resource")
}
async fn plan_resource_change(
&self,
request: tonic::Request<tfplugin6::plan_resource_change::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::plan_resource_change::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::plan_resource_change::Request>,
) -> Result<Response<tfplugin6::plan_resource_change::Response>, Status> {
tracing::error!("plan_resource_change");
let reply = tfplugin6::plan_resource_change::Response {
planned_state: todo!(),
requires_replace: vec![],
planned_private: vec![],
diagnostics: vec![],
legacy_type_system: false,
deferred: None,
};
todo!("plan_resource_change")
}
async fn apply_resource_change(
&self,
request: tonic::Request<tfplugin6::apply_resource_change::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::apply_resource_change::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::apply_resource_change::Request>,
) -> Result<Response<tfplugin6::apply_resource_change::Response>, Status> {
tracing::error!("apply_resource_change");
todo!("apply_resource_change")
}
async fn import_resource_state(
&self,
request: tonic::Request<tfplugin6::import_resource_state::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::import_resource_state::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::import_resource_state::Request>,
) -> Result<Response<tfplugin6::import_resource_state::Response>, Status> {
tracing::error!("import_resource_state");
todo!("import_resource_state")
}
async fn move_resource_state(
&self,
request: tonic::Request<tfplugin6::move_resource_state::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::move_resource_state::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::move_resource_state::Request>,
) -> Result<Response<tfplugin6::move_resource_state::Response>, Status> {
tracing::error!("move_resource_state");
todo!("move_resource_state")
}
async fn read_data_source(
&self,
request: tonic::Request<tfplugin6::read_data_source::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::read_data_source::Response>, tonic::Status>
{
todo!()
request: Request<tfplugin6::read_data_source::Request>,
) -> Result<Response<tfplugin6::read_data_source::Response>, Status> {
tracing::error!("read_data_source");
todo!("read_data_source")
}
/// GetFunctions returns the definitions of all functions.
async fn get_functions(
&self,
request: tonic::Request<tfplugin6::get_functions::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::get_functions::Response>, tonic::Status> {
todo!()
request: Request<tfplugin6::get_functions::Request>,
) -> Result<Response<tfplugin6::get_functions::Response>, Status> {
tracing::error!("get_functions");
todo!("get_functions")
}
/// ////// Provider-contributed Functions
async fn call_function(
&self,
request: tonic::Request<tfplugin6::call_function::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::call_function::Response>, tonic::Status> {
todo!()
request: Request<tfplugin6::call_function::Request>,
) -> Result<Response<tfplugin6::call_function::Response>, Status> {
tracing::error!("call_function");
todo!("call_function")
}
/// ////// Graceful Shutdown
async fn stop_provider(
&self,
request: tonic::Request<tfplugin6::stop_provider::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::stop_provider::Response>, tonic::Status> {
todo!()
request: Request<tfplugin6::stop_provider::Request>,
) -> Result<Response<tfplugin6::stop_provider::Response>, Status> {
tracing::error!("stop_provider");
todo!("stop_provider")
}
}