Skip to content

Commit 0b80c8f

Browse files
committed
rudimentary receiver create
gotta redo this to take the params but need to rebase
1 parent 3851a40 commit 0b80c8f

File tree

8 files changed

+206
-29
lines changed

8 files changed

+206
-29
lines changed

common/src/api/external/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,7 @@ pub enum ResourceType {
10101010
FloatingIp,
10111011
Probe,
10121012
ProbeNetworkInterface,
1013+
WebhookReceiver,
10131014
}
10141015

10151016
// IDENTITY METADATA

nexus/auth/src/authz/api_resources.rs

+9
Original file line numberDiff line numberDiff line change
@@ -1035,3 +1035,12 @@ authz_resource! {
10351035
roles_allowed = false,
10361036
polar_snippet = FleetChild,
10371037
}
1038+
1039+
authz_resource! {
1040+
name = "WebhookReceiver",
1041+
parent = "Fleet",
1042+
primary_key = { uuid_kind = WebhookReceiverKind },
1043+
roles_allowed = false,
1044+
polar_snippet = FleetChild,
1045+
1046+
}

nexus/db-model/src/schema.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -2114,11 +2114,12 @@ table! {
21142114
id -> Uuid,
21152115
name -> Text,
21162116
description -> Text,
2117-
endpoint -> Text,
2118-
probes_enabled -> Bool,
21192117
time_created -> Timestamptz,
21202118
time_modified -> Nullable<Timestamptz>,
21212119
time_deleted -> Nullable<Timestamptz>,
2120+
rcgen -> Int8,
2121+
endpoint -> Text,
2122+
probes_enabled -> Bool,
21222123
}
21232124
}
21242125

nexus/db-model/src/webhook_rx.rs

+58-18
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
use crate::collection::DatastoreCollectionConfig;
56
use crate::schema::{webhook_rx, webhook_rx_secret, webhook_rx_subscription};
67
use crate::typed_uuid::DbTypedUuid;
8+
use crate::Generation;
79
use chrono::{DateTime, Utc};
810
use db_macros::Resource;
11+
use omicron_common::api::external::Error;
912
use omicron_uuid_kinds::{WebhookReceiverKind, WebhookReceiverUuid};
1013
use serde::{Deserialize, Serialize};
14+
use std::str::FromStr;
15+
use uuid::Uuid;
1116

1217
/// A webhook receiver configuration.
1318
#[derive(
@@ -23,11 +28,31 @@ use serde::{Deserialize, Serialize};
2328
#[diesel(table_name = webhook_rx)]
2429
pub struct WebhookReceiver {
2530
#[diesel(embed)]
26-
identity: WebhookReceiverIdentity,
31+
pub identity: WebhookReceiverIdentity,
2732
pub probes_enabled: bool,
2833
pub endpoint: String,
34+
35+
/// child resource generation number, per RFD 192
36+
pub rcgen: Generation,
37+
}
38+
39+
impl DatastoreCollectionConfig<WebhookRxSecret> for WebhookReceiver {
40+
type CollectionId = Uuid;
41+
type GenerationNumberColumn = webhook_rx::dsl::rcgen;
42+
type CollectionTimeDeletedColumn = webhook_rx::dsl::time_deleted;
43+
type CollectionIdColumn = webhook_rx_secret::dsl::rx_id;
44+
}
45+
46+
impl DatastoreCollectionConfig<WebhookRxSubscription> for WebhookReceiver {
47+
type CollectionId = Uuid;
48+
type GenerationNumberColumn = webhook_rx::dsl::rcgen;
49+
type CollectionTimeDeletedColumn = webhook_rx::dsl::time_deleted;
50+
type CollectionIdColumn = webhook_rx_subscription::dsl::rx_id;
2951
}
3052

53+
// TODO(eliza): should deliveries/delivery attempts also be treated as children
54+
// of a webhook receiver?
55+
3156
#[derive(
3257
Clone, Debug, Queryable, Selectable, Insertable, Serialize, Deserialize,
3358
)]
@@ -46,13 +71,23 @@ pub struct WebhookRxSecret {
4671
#[diesel(table_name = webhook_rx_subscription)]
4772
pub struct WebhookRxSubscription {
4873
pub rx_id: DbTypedUuid<WebhookReceiverKind>,
74+
#[diesel(embed)]
75+
pub glob: WebhookGlob,
76+
pub time_created: DateTime<Utc>,
77+
}
78+
79+
#[derive(
80+
Clone, Debug, Queryable, Selectable, Insertable, Serialize, Deserialize,
81+
)]
82+
#[diesel(table_name = webhook_rx_subscription)]
83+
pub struct WebhookGlob {
4984
pub event_class: String,
5085
pub similar_to: String,
51-
pub time_created: DateTime<Utc>,
5286
}
5387

54-
impl WebhookRxSubscription {
55-
pub fn new(rx_id: WebhookReceiverUuid, event_class: String) -> Self {
88+
impl FromStr for WebhookGlob {
89+
type Err = Error;
90+
fn from_str(event_class: &str) -> Result<Self, Self::Err> {
5691
fn seg2regex(segment: &str, similar_to: &mut String) {
5792
match segment {
5893
// Match one segment (i.e. any number of segment characters)
@@ -63,6 +98,7 @@ impl WebhookRxSubscription {
6398
// Because `_` his a metacharacter in Postgres' SIMILAR TO
6499
// regexes, we've gotta go through and escape them.
65100
s => {
101+
// TODO(eliza): validate what characters are in the segment...
66102
for s in s.split_inclusive('_') {
67103
// Handle the fact that there might not be a `_` in the
68104
// string at all
@@ -87,19 +123,19 @@ impl WebhookRxSubscription {
87123
seg2regex(segment, &mut similar_to);
88124
}
89125
} else {
90-
// TODO(eliza): we should probably validate that the event class has
91-
// at least one segment...
126+
return Err(Error::invalid_value(
127+
"event_class",
128+
"must not be empty",
129+
));
92130
};
93131

94-
// `_` is a metacharacter in Postgres' SIMILAR TO regexes, so escape
95-
// them.
132+
Ok(Self { event_class: event_class.to_string(), similar_to })
133+
}
134+
}
96135

97-
Self {
98-
rx_id: DbTypedUuid(rx_id),
99-
event_class,
100-
similar_to,
101-
time_created: Utc::now(),
102-
}
136+
impl WebhookRxSubscription {
137+
pub fn new(rx_id: WebhookReceiverUuid, glob: WebhookGlob) -> Self {
138+
Self { rx_id: DbTypedUuid(rx_id), glob, time_created: Utc::now() }
103139
}
104140
}
105141

@@ -119,13 +155,17 @@ mod test {
119155
("foo_bar.baz", "foo\\_bar.baz"),
120156
("foo_bar.*.baz", "foo\\_bar.[a-zA-Z0-9\\_\\-]+.baz"),
121157
];
122-
let rx_id = WebhookReceiverUuid::new_v4();
123158
for (class, regex) in CASES {
124-
let subscription =
125-
WebhookRxSubscription::new(rx_id, dbg!(class).to_string());
159+
let glob = match WebhookGlob::from_str(dbg!(class)) {
160+
Ok(glob) => glob,
161+
Err(error) => panic!(
162+
"event class glob {class:?} should produce the regex
163+
{regex:?}, but instead failed to parse: {error}"
164+
),
165+
};
126166
assert_eq!(
127167
dbg!(regex),
128-
dbg!(&subscription.similar_to),
168+
dbg!(&glob.similar_to),
129169
"event class {class:?} should produce the regex {regex:?}"
130170
);
131171
}

nexus/db-queries/src/db/datastore/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ mod volume;
107107
mod volume_repair;
108108
mod vpc;
109109
mod webhook_event;
110+
mod webhook_rx;
110111
mod zpool;
111112

112113
pub use address_lot::AddressLotCreateResult;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! [`DataStore`] methods for webhook receiver management.
6+
7+
use super::DataStore;
8+
use crate::authz;
9+
use crate::context::OpContext;
10+
use crate::db::collection_insert::AsyncInsertError;
11+
use crate::db::collection_insert::DatastoreCollection;
12+
use crate::db::error::public_error_from_diesel;
13+
use crate::db::error::retryable;
14+
use crate::db::error::ErrorHandler;
15+
use crate::db::model::WebhookGlob;
16+
use crate::db::model::WebhookReceiver;
17+
use crate::db::model::WebhookRxSubscription;
18+
use crate::db::pool::DbConnection;
19+
use crate::db::TransactionError;
20+
use async_bb8_diesel::AsyncConnection;
21+
use async_bb8_diesel::AsyncRunQueryDsl;
22+
use diesel::prelude::*;
23+
use diesel::result::OptionalExtension;
24+
use nexus_types::identity::Resource;
25+
use omicron_common::api::external::CreateResult;
26+
use omicron_common::api::external::DeleteResult;
27+
use omicron_common::api::external::Error;
28+
use omicron_common::api::external::LookupType;
29+
use omicron_common::api::external::ResourceType;
30+
use omicron_uuid_kinds::{GenericUuid, WebhookReceiverUuid};
31+
32+
impl DataStore {
33+
pub async fn webhook_rx_create(
34+
&self,
35+
opctx: &OpContext,
36+
receiver: &WebhookReceiver,
37+
subscriptions: &[WebhookGlob],
38+
) -> CreateResult<WebhookReceiver> {
39+
use crate::db::schema::webhook_rx::dsl;
40+
// TODO(eliza): someday we gotta allow creating webhooks with more
41+
// restrictive permissions...
42+
opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?;
43+
44+
let conn = self.pool_connection_authorized(opctx).await?;
45+
let rx_id = WebhookReceiverUuid::from_untyped_uuid(receiver.id());
46+
self.transaction_retry_wrapper("webhook_rx_create")
47+
.transaction(&conn, |conn| {
48+
let receiver = receiver.clone();
49+
async move {
50+
diesel::insert_into(dsl::webhook_rx)
51+
.values(receiver)
52+
// .on_conflict(dsl::id)
53+
// .do_update()
54+
// .set(dsl::time_modified.eq(dsl::time_modified))
55+
// .returning(WebhookReceiver::as_returning())
56+
.execute_async(&conn)
57+
.await?;
58+
// .map_err(|e| {
59+
// if retryable(&e) {
60+
// return TransactionError::Database(e);
61+
// };
62+
// TransactionError::CustomError(public_error_from_diesel(
63+
// e,
64+
// ErrorHandler::Conflict(
65+
// ResourceType::WebhookReceiver,
66+
// receiver.identity.name.as_str(),
67+
// ),
68+
// ))
69+
// })?;
70+
for glob in subscriptions {
71+
match self
72+
.webhook_add_subscription_on_conn(
73+
WebhookRxSubscription::new(rx_id, glob.clone()),
74+
&conn,
75+
)
76+
.await
77+
{
78+
Ok(_) => {}
79+
Err(AsyncInsertError::CollectionNotFound) => {} // we just created it?
80+
Err(AsyncInsertError::DatabaseError(e)) => {
81+
return Err(e);
82+
}
83+
}
84+
}
85+
// TODO(eliza): secrets go here...
86+
Ok(())
87+
}
88+
})
89+
.await
90+
.map_err(|e| {
91+
public_error_from_diesel(
92+
e,
93+
ErrorHandler::Conflict(
94+
ResourceType::WebhookReceiver,
95+
receiver.name().as_str(),
96+
),
97+
)
98+
})?;
99+
Ok(receiver.clone())
100+
}
101+
102+
async fn webhook_add_subscription_on_conn(
103+
&self,
104+
subscription: WebhookRxSubscription,
105+
conn: &async_bb8_diesel::Connection<DbConnection>,
106+
) -> Result<WebhookRxSubscription, AsyncInsertError> {
107+
use crate::db::schema::webhook_rx_subscription::dsl;
108+
let rx_id = subscription.rx_id.into_untyped_uuid();
109+
let subscription: WebhookRxSubscription =
110+
WebhookReceiver::insert_resource(
111+
rx_id,
112+
diesel::insert_into(dsl::webhook_rx_subscription)
113+
.values(subscription)
114+
.on_conflict((dsl::rx_id, dsl::event_class))
115+
.do_update()
116+
.set(dsl::time_created.eq(diesel::dsl::now)),
117+
)
118+
.insert_and_get_result_async(conn)
119+
.await?;
120+
Ok(subscription)
121+
}
122+
}

schema/crdb/add-webhooks/up01.sql

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx (
2+
/* Identity metadata (resource) */
23
id UUID PRIMARY KEY,
3-
-- A human-readable identifier for this webhook receiver.
44
name STRING(63) NOT NULL,
55
description STRING(512) NOT NULL,
6+
time_created TIMESTAMPTZ NOT NULL,
7+
time_modified TIMESTAMPTZ NOT NULL,
8+
time_deleted TIMESTAMPTZ,
9+
-- Child resource generation
10+
rcgen INT NOT NULL,
611
-- URL of the endpoint webhooks are delivered to.
712
endpoint STRING(512) NOT NULL,
813
-- Whether or not liveness probes are sent to this receiver.
914
probes_enabled BOOL NOT NULL,
10-
-- TODO(eliza): how do we track which roles are assigned to a webhook?
11-
time_created TIMESTAMPTZ NOT NULL,
12-
time_deleted TIMESTAMPTZ
1315
);

schema/crdb/dbinit.sql

+6-5
Original file line numberDiff line numberDiff line change
@@ -4693,18 +4693,19 @@ CREATE UNIQUE INDEX IF NOT EXISTS one_record_per_volume_resource_usage on omicro
46934693
*/
46944694

46954695
CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx (
4696+
/* Identity metadata (resource) */
46964697
id UUID PRIMARY KEY,
4697-
-- A human-readable identifier for this webhook receiver.
46984698
name STRING(63) NOT NULL,
46994699
description STRING(512) NOT NULL,
4700+
time_created TIMESTAMPTZ NOT NULL,
4701+
time_modified TIMESTAMPTZ NOT NULL,
4702+
time_deleted TIMESTAMPTZ,
4703+
-- Child resource generation
4704+
rcgen INT NOT NULL,
47004705
-- URL of the endpoint webhooks are delivered to.
47014706
endpoint STRING(512) NOT NULL,
47024707
-- Whether or not liveness probes are sent to this receiver.
47034708
probes_enabled BOOL NOT NULL,
4704-
-- TODO(eliza): how do we track which roles are assigned to a webhook?
4705-
time_created TIMESTAMPTZ NOT NULL,
4706-
time_modified TIMESTAMPTZ,
4707-
time_deleted TIMESTAMPTZ
47084709
);
47094710

47104711
CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_secret (

0 commit comments

Comments
 (0)