Skip to content

Commit 0ea7f1d

Browse files
committed
PostgreSQL certs from guest mount
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
1 parent a5ef3c8 commit 0ea7f1d

16 files changed

Lines changed: 420 additions & 171 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/factor-outbound-pg/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@ native-tls = "0.2"
1414
postgres-native-tls = "0.5"
1515
postgres_range = "0.11"
1616
rust_decimal = { version = "1.37", features = ["db-tokio-postgres"] }
17+
tokio-rustls = { workspace = true }
1718
serde_json = { workspace = true }
19+
spin-common = { path = "../common" }
1820
spin-core = { path = "../core" }
1921
spin-factor-otel = { path = "../factor-otel" }
2022
spin-factor-outbound-networking = { path = "../factor-outbound-networking" }
2123
spin-factors = { path = "../factors" }
24+
spin-locked-app = { path = "../locked-app" }
2225
spin-resource-table = { path = "../table" }
2326
spin-world = { path = "../world" }
2427
tokio = { workspace = true, features = ["rt-multi-thread"] }
2528
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] }
2629
tracing = { workspace = true }
30+
url = { workspace = true }
2731
uuid = "1"
2832

2933
[dev-dependencies]

crates/factor-outbound-pg/src/client.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ use anyhow::{Context, Result};
22
use native_tls::TlsConnector;
33
use postgres_native_tls::MakeTlsConnector;
44
use spin_world::async_trait;
5-
use spin_world::spin::postgres4_0_0::postgres::{
5+
use spin_world::spin::postgres4_1_0::postgres::{
66
self as v4, Column, DbValue, ParameterValue, RowSet,
77
};
8+
use tokio_postgres::config::SslMode;
89
use tokio_postgres::types::ToSql;
9-
use tokio_postgres::{config::SslMode, NoTls, Row};
10+
use tokio_postgres::{NoTls, Row};
1011

1112
use crate::types::{convert_data_type, convert_entry, to_sql_parameter};
1213

@@ -22,12 +23,12 @@ pub trait ClientFactory: Default + Send + Sync + 'static {
2223
/// The type of client produced by `get_client`.
2324
type Client: Client;
2425
/// Gets a client from the factory.
25-
async fn get_client(&self, address: &str) -> Result<Self::Client>;
26+
async fn get_client(&self, address: &str, root_ca: Option<&String>) -> Result<Self::Client>;
2627
}
2728

2829
/// A `ClientFactory` that uses a connection pool per address.
2930
pub struct PooledTokioClientFactory {
30-
pools: moka::sync::Cache<String, deadpool_postgres::Pool>,
31+
pools: moka::sync::Cache<(String, Option<String>), deadpool_postgres::Pool>,
3132
}
3233

3334
impl Default for PooledTokioClientFactory {
@@ -42,10 +43,11 @@ impl Default for PooledTokioClientFactory {
4243
impl ClientFactory for PooledTokioClientFactory {
4344
type Client = deadpool_postgres::Object;
4445

45-
async fn get_client(&self, address: &str) -> Result<Self::Client> {
46+
async fn get_client(&self, address: &str, root_ca: Option<&String>) -> Result<Self::Client> {
47+
let pool_key = (address.to_string(), root_ca.cloned());
4648
let pool = self
4749
.pools
48-
.try_get_with_by_ref(address, || create_connection_pool(address))
50+
.try_get_with_by_ref(&pool_key, || create_connection_pool(address, root_ca))
4951
.map_err(ArcError)
5052
.context("establishing PostgreSQL connection pool")?;
5153

@@ -54,7 +56,10 @@ impl ClientFactory for PooledTokioClientFactory {
5456
}
5557

5658
/// Creates a Postgres connection pool for the given address.
57-
fn create_connection_pool(address: &str) -> Result<deadpool_postgres::Pool> {
59+
fn create_connection_pool(
60+
address: &str,
61+
root_ca: Option<&String>,
62+
) -> Result<deadpool_postgres::Pool> {
5863
let config = address
5964
.parse::<tokio_postgres::Config>()
6065
.context("parsing Postgres connection string")?;
@@ -68,7 +73,11 @@ fn create_connection_pool(address: &str) -> Result<deadpool_postgres::Pool> {
6873
let mgr = if config.get_ssl_mode() == SslMode::Disable {
6974
deadpool_postgres::Manager::from_config(config, NoTls, mgr_config)
7075
} else {
71-
let builder = TlsConnector::builder();
76+
let mut builder = TlsConnector::builder();
77+
if let Some(root_ca) = root_ca {
78+
let cert_bytes = root_ca.as_bytes();
79+
builder.add_root_certificate(native_tls::Certificate::from_pem(cert_bytes)?);
80+
}
7281
let connector = MakeTlsConnector::new(builder.build()?);
7382
deadpool_postgres::Manager::from_config(config, connector, mgr_config)
7483
};

crates/factor-outbound-pg/src/host.rs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::Result;
22
use spin_core::wasmtime::component::Resource;
33
use spin_world::spin::postgres3_0_0::postgres::{self as v3};
4-
use spin_world::spin::postgres4_0_0::postgres::{self as v4};
4+
use spin_world::spin::postgres4_1_0::postgres::{self as v4};
55
use spin_world::v1::postgres as v1;
66
use spin_world::v1::rdbms_types as v1_types;
77
use spin_world::v2::postgres::{self as v2};
@@ -17,11 +17,12 @@ impl<CF: ClientFactory> InstanceState<CF> {
1717
async fn open_connection<Conn: 'static>(
1818
&mut self,
1919
address: &str,
20+
root_ca: Option<&String>,
2021
) -> Result<Resource<Conn>, v4::Error> {
2122
self.connections
2223
.push(
2324
self.client_factory
24-
.get_client(address)
25+
.get_client(address, root_ca)
2526
.await
2627
.map_err(|e| v4::Error::ConnectionFailed(format!("{e:?}")))?,
2728
)
@@ -102,7 +103,7 @@ impl<CF: ClientFactory> v3::HostConnection for InstanceState<CF> {
102103

103104
self.ensure_address_allowed(&address).await?;
104105

105-
Ok(self.open_connection(&address).await?)
106+
Ok(self.open_connection(&address, None).await?)
106107
}
107108

108109
#[instrument(name = "spin_outbound_pg.execute", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))]
@@ -140,14 +141,67 @@ impl<CF: ClientFactory> v3::HostConnection for InstanceState<CF> {
140141
}
141142
}
142143

144+
pub(crate) struct ConnectionBuilder {
145+
address: String,
146+
root_ca: Option<String>,
147+
}
148+
149+
impl<CF: ClientFactory> v4::HostConnectionBuilder for InstanceState<CF> {
150+
async fn new(&mut self, address: String) -> Result<Resource<v4::ConnectionBuilder>> {
151+
let builder = ConnectionBuilder {
152+
address,
153+
root_ca: None,
154+
};
155+
let rep = self
156+
.builders
157+
.push(builder)
158+
.map_err(|_| anyhow::anyhow!("out of builder table space"))?;
159+
let rsrc = Resource::new_own(rep);
160+
Ok(rsrc)
161+
}
162+
163+
async fn set_ca_root(
164+
&mut self,
165+
self_: Resource<v4::ConnectionBuilder>,
166+
certificate: String,
167+
) -> Result<(), v4::Error> {
168+
let builder = self
169+
.builders
170+
.get_mut(self_.rep())
171+
.ok_or_else(|| v4::Error::ConnectionFailed("no builder found".into()))?;
172+
builder.root_ca = Some(certificate);
173+
Ok(())
174+
}
175+
176+
async fn build(
177+
&mut self,
178+
self_: Resource<v4::ConnectionBuilder>,
179+
) -> Result<Resource<v4::Connection>, v4::Error> {
180+
let builder = self
181+
.builders
182+
.get_mut(self_.rep())
183+
.ok_or_else(|| v4::Error::ConnectionFailed("no builder found".into()))?;
184+
// borrow checker gets pedantic here, so we need to outsmart it
185+
let address = builder.address.clone();
186+
let root_ca = builder.root_ca.clone();
187+
let conn = self.open_connection(&address, root_ca.as_ref()).await;
188+
conn
189+
}
190+
191+
async fn drop(&mut self, builder: Resource<v4::ConnectionBuilder>) -> Result<()> {
192+
self.builders.remove(builder.rep());
193+
Ok(())
194+
}
195+
}
196+
143197
impl<CF: ClientFactory> v4::HostConnection for InstanceState<CF> {
144198
#[instrument(name = "spin_outbound_pg.open", skip(self, address), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", db.address = Empty, server.port = Empty, db.namespace = Empty))]
145199
async fn open(&mut self, address: String) -> Result<Resource<v4::Connection>, v4::Error> {
146200
spin_factor_outbound_networking::record_address_fields(&address);
147201

148202
self.ensure_address_allowed(&address).await?;
149203

150-
self.open_connection(&address).await
204+
self.open_connection(&address, None).await
151205
}
152206

153207
#[instrument(name = "spin_outbound_pg.execute", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))]
@@ -204,7 +258,7 @@ impl<CF: ClientFactory> v4::Host for InstanceState<CF> {
204258
macro_rules! delegate {
205259
($self:ident.$name:ident($address:expr, $($arg:expr),*)) => {{
206260
$self.ensure_address_allowed(&$address).await?;
207-
let connection = match $self.open_connection(&$address).await {
261+
let connection = match $self.open_connection(&$address, None).await {
208262
Ok(c) => c,
209263
Err(e) => return Err(e.into()),
210264
};
@@ -224,7 +278,7 @@ impl<CF: ClientFactory> v2::HostConnection for InstanceState<CF> {
224278

225279
self.ensure_address_allowed(&address).await?;
226280

227-
Ok(self.open_connection(&address).await?)
281+
Ok(self.open_connection(&address, None).await?)
228282
}
229283

230284
#[instrument(name = "spin_outbound_pg.execute", skip(self, connection, params), err(level = Level::INFO), fields(otel.kind = "client", db.system = "postgresql", otel.name = statement))]

crates/factor-outbound-pg/src/lib.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub mod client;
22
mod host;
33
mod types;
44

5+
use std::{collections::HashMap, sync::Arc};
6+
57
use client::ClientFactory;
68
use spin_factor_otel::OtelFactorState;
79
use spin_factor_outbound_networking::{
@@ -11,15 +13,14 @@ use spin_factors::{
1113
anyhow, ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors,
1214
SelfInstanceBuilder,
1315
};
14-
use std::sync::Arc;
1516

1617
pub struct OutboundPgFactor<CF = crate::client::PooledTokioClientFactory> {
1718
_phantom: std::marker::PhantomData<CF>,
1819
}
1920

2021
impl<CF: ClientFactory> Factor for OutboundPgFactor<CF> {
2122
type RuntimeConfig = ();
22-
type AppState = Arc<CF>;
23+
type AppState = HashMap<String, Arc<CF>>;
2324
type InstanceBuilder = InstanceState<CF>;
2425

2526
fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> {
@@ -29,16 +30,20 @@ impl<CF: ClientFactory> Factor for OutboundPgFactor<CF> {
2930
spin_world::spin::postgres3_0_0::postgres::add_to_linker::<_, FactorData<Self>>,
3031
)?;
3132
ctx.link_bindings(
32-
spin_world::spin::postgres4_0_0::postgres::add_to_linker::<_, FactorData<Self>>,
33+
spin_world::spin::postgres4_1_0::postgres::add_to_linker::<_, FactorData<Self>>,
3334
)?;
3435
Ok(())
3536
}
3637

3738
fn configure_app<T: RuntimeFactors>(
3839
&self,
39-
_ctx: ConfigureAppContext<T, Self>,
40+
ctx: ConfigureAppContext<T, Self>,
4041
) -> anyhow::Result<Self::AppState> {
41-
Ok(Arc::new(CF::default()))
42+
let mut client_factories = HashMap::new();
43+
for comp in ctx.app().components() {
44+
client_factories.insert(comp.id().to_string(), Arc::new(CF::default()));
45+
}
46+
Ok(client_factories)
4247
}
4348

4449
fn prepare<T: RuntimeFactors>(
@@ -49,12 +54,14 @@ impl<CF: ClientFactory> Factor for OutboundPgFactor<CF> {
4954
.instance_builder::<OutboundNetworkingFactor>()?
5055
.allowed_hosts();
5156
let otel = OtelFactorState::from_prepare_context(&mut ctx)?;
57+
let cf = ctx.app_state().get(ctx.app_component().id()).unwrap();
5258

5359
Ok(InstanceState {
5460
allowed_hosts,
55-
client_factory: ctx.app_state().clone(),
61+
client_factory: cf.clone(),
5662
connections: Default::default(),
5763
otel,
64+
builders: Default::default(),
5865
})
5966
}
6067
}
@@ -78,6 +85,7 @@ pub struct InstanceState<CF: ClientFactory> {
7885
client_factory: Arc<CF>,
7986
connections: spin_resource_table::Table<CF::Client>,
8087
otel: OtelFactorState,
88+
builders: spin_resource_table::Table<host::ConnectionBuilder>,
8189
}
8290

8391
impl<CF: ClientFactory> SelfInstanceBuilder for InstanceState<CF> {}

crates/factor-outbound-pg/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use spin_world::spin::postgres4_0_0::postgres::{DbDataType, DbValue, ParameterValue};
1+
use spin_world::spin::postgres4_1_0::postgres::{DbDataType, DbValue, ParameterValue};
22
use tokio_postgres::types::{FromSql, Type};
33
use tokio_postgres::{types::ToSql, Row};
44

crates/factor-outbound-pg/src/types/convert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! the tokio_postgres driver.
33
44
use anyhow::{anyhow, Context};
5-
use spin_world::spin::postgres4_0_0::postgres::{self as v4};
5+
use spin_world::spin::postgres4_1_0::postgres::{self as v4};
66

77
use super::decimal::RangeableDecimal;
88

crates/factor-outbound-pg/src/types/interval.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::Result;
2-
use spin_world::spin::postgres4_0_0::postgres::{self as v4};
2+
use spin_world::spin::postgres4_1_0::postgres::{self as v4};
33
use tokio_postgres::types::{FromSql, ToSql, Type};
44

55
#[derive(Debug)]

crates/factor-outbound-pg/tests/factor_test.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ use spin_factor_variables::VariablesFactor;
77
use spin_factors::{anyhow, RuntimeFactors};
88
use spin_factors_test::{toml, TestEnvironment};
99
use spin_world::async_trait;
10-
use spin_world::spin::postgres4_0_0::postgres::Error as PgError;
11-
use spin_world::spin::postgres4_0_0::postgres::HostConnection;
12-
use spin_world::spin::postgres4_0_0::postgres::{self as v2};
13-
use spin_world::spin::postgres4_0_0::postgres::{ParameterValue, RowSet};
10+
use spin_world::spin::postgres4_1_0::postgres::Error as PgError;
11+
use spin_world::spin::postgres4_1_0::postgres::HostConnection;
12+
use spin_world::spin::postgres4_1_0::postgres::{self as v2};
13+
use spin_world::spin::postgres4_1_0::postgres::{ParameterValue, RowSet};
1414

1515
#[derive(RuntimeFactors)]
1616
struct TestFactors {
@@ -112,7 +112,7 @@ pub struct MockClient {}
112112
#[async_trait]
113113
impl ClientFactory for MockClientFactory {
114114
type Client = MockClient;
115-
async fn get_client(&self, _address: &str) -> Result<Self::Client> {
115+
async fn get_client(&self, _address: &str, _root_ca: Option<&String>) -> Result<Self::Client> {
116116
Ok(MockClient {})
117117
}
118118
}

crates/world/src/conversions.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use super::*;
33
mod rdbms_types {
44
use super::*;
55
use spin::postgres3_0_0::postgres as pg3;
6-
use spin::postgres4_0_0::postgres as pg4;
6+
use spin::postgres4_1_0::postgres as pg4;
77

88
impl From<v2::rdbms_types::Column> for v1::rdbms_types::Column {
99
fn from(value: v2::rdbms_types::Column) -> Self {
@@ -15,7 +15,7 @@ mod rdbms_types {
1515
}
1616

1717
impl From<pg4::Column> for v1::rdbms_types::Column {
18-
fn from(value: spin::postgres4_0_0::postgres::Column) -> Self {
18+
fn from(value: pg4::Column) -> Self {
1919
v1::rdbms_types::Column {
2020
name: value.name,
2121
data_type: value.data_type.into(),
@@ -422,7 +422,7 @@ mod rdbms_types {
422422
mod postgres {
423423
use super::*;
424424
use spin::postgres3_0_0::postgres as pg3;
425-
use spin::postgres4_0_0::postgres as pg4;
425+
use spin::postgres4_1_0::postgres as pg4;
426426

427427
impl From<pg4::RowSet> for v1::postgres::RowSet {
428428
fn from(value: pg4::RowSet) -> v1::postgres::RowSet {

0 commit comments

Comments
 (0)