Skip to content

Commit

Permalink
rudimentary receiver create
Browse files Browse the repository at this point in the history
gotta redo this to take the params but need to rebase
  • Loading branch information
hawkw committed Jan 8, 2025
1 parent 3851a40 commit 0b80c8f
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 29 deletions.
1 change: 1 addition & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,7 @@ pub enum ResourceType {
FloatingIp,
Probe,
ProbeNetworkInterface,
WebhookReceiver,
}

// IDENTITY METADATA
Expand Down
9 changes: 9 additions & 0 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1035,3 +1035,12 @@ authz_resource! {
roles_allowed = false,
polar_snippet = FleetChild,
}

authz_resource! {
name = "WebhookReceiver",
parent = "Fleet",
primary_key = { uuid_kind = WebhookReceiverKind },
roles_allowed = false,
polar_snippet = FleetChild,

}
5 changes: 3 additions & 2 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2114,11 +2114,12 @@ table! {
id -> Uuid,
name -> Text,
description -> Text,
endpoint -> Text,
probes_enabled -> Bool,
time_created -> Timestamptz,
time_modified -> Nullable<Timestamptz>,
time_deleted -> Nullable<Timestamptz>,
rcgen -> Int8,
endpoint -> Text,
probes_enabled -> Bool,
}
}

Expand Down
76 changes: 58 additions & 18 deletions nexus/db-model/src/webhook_rx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::collection::DatastoreCollectionConfig;
use crate::schema::{webhook_rx, webhook_rx_secret, webhook_rx_subscription};
use crate::typed_uuid::DbTypedUuid;
use crate::Generation;
use chrono::{DateTime, Utc};
use db_macros::Resource;
use omicron_common::api::external::Error;
use omicron_uuid_kinds::{WebhookReceiverKind, WebhookReceiverUuid};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use uuid::Uuid;

/// A webhook receiver configuration.
#[derive(
Expand All @@ -23,11 +28,31 @@ use serde::{Deserialize, Serialize};
#[diesel(table_name = webhook_rx)]
pub struct WebhookReceiver {
#[diesel(embed)]
identity: WebhookReceiverIdentity,
pub identity: WebhookReceiverIdentity,
pub probes_enabled: bool,
pub endpoint: String,

/// child resource generation number, per RFD 192
pub rcgen: Generation,
}

impl DatastoreCollectionConfig<WebhookRxSecret> for WebhookReceiver {
type CollectionId = Uuid;
type GenerationNumberColumn = webhook_rx::dsl::rcgen;
type CollectionTimeDeletedColumn = webhook_rx::dsl::time_deleted;
type CollectionIdColumn = webhook_rx_secret::dsl::rx_id;
}

impl DatastoreCollectionConfig<WebhookRxSubscription> for WebhookReceiver {
type CollectionId = Uuid;
type GenerationNumberColumn = webhook_rx::dsl::rcgen;
type CollectionTimeDeletedColumn = webhook_rx::dsl::time_deleted;
type CollectionIdColumn = webhook_rx_subscription::dsl::rx_id;
}

// TODO(eliza): should deliveries/delivery attempts also be treated as children
// of a webhook receiver?

#[derive(
Clone, Debug, Queryable, Selectable, Insertable, Serialize, Deserialize,
)]
Expand All @@ -46,13 +71,23 @@ pub struct WebhookRxSecret {
#[diesel(table_name = webhook_rx_subscription)]
pub struct WebhookRxSubscription {
pub rx_id: DbTypedUuid<WebhookReceiverKind>,
#[diesel(embed)]
pub glob: WebhookGlob,
pub time_created: DateTime<Utc>,
}

#[derive(
Clone, Debug, Queryable, Selectable, Insertable, Serialize, Deserialize,
)]
#[diesel(table_name = webhook_rx_subscription)]
pub struct WebhookGlob {
pub event_class: String,
pub similar_to: String,
pub time_created: DateTime<Utc>,
}

impl WebhookRxSubscription {
pub fn new(rx_id: WebhookReceiverUuid, event_class: String) -> Self {
impl FromStr for WebhookGlob {
type Err = Error;
fn from_str(event_class: &str) -> Result<Self, Self::Err> {
fn seg2regex(segment: &str, similar_to: &mut String) {
match segment {
// Match one segment (i.e. any number of segment characters)
Expand All @@ -63,6 +98,7 @@ impl WebhookRxSubscription {
// Because `_` his a metacharacter in Postgres' SIMILAR TO
// regexes, we've gotta go through and escape them.
s => {
// TODO(eliza): validate what characters are in the segment...
for s in s.split_inclusive('_') {
// Handle the fact that there might not be a `_` in the
// string at all
Expand All @@ -87,19 +123,19 @@ impl WebhookRxSubscription {
seg2regex(segment, &mut similar_to);
}
} else {
// TODO(eliza): we should probably validate that the event class has
// at least one segment...
return Err(Error::invalid_value(
"event_class",
"must not be empty",
));
};

// `_` is a metacharacter in Postgres' SIMILAR TO regexes, so escape
// them.
Ok(Self { event_class: event_class.to_string(), similar_to })
}
}

Self {
rx_id: DbTypedUuid(rx_id),
event_class,
similar_to,
time_created: Utc::now(),
}
impl WebhookRxSubscription {
pub fn new(rx_id: WebhookReceiverUuid, glob: WebhookGlob) -> Self {
Self { rx_id: DbTypedUuid(rx_id), glob, time_created: Utc::now() }
}
}

Expand All @@ -119,13 +155,17 @@ mod test {
("foo_bar.baz", "foo\\_bar.baz"),
("foo_bar.*.baz", "foo\\_bar.[a-zA-Z0-9\\_\\-]+.baz"),
];
let rx_id = WebhookReceiverUuid::new_v4();
for (class, regex) in CASES {
let subscription =
WebhookRxSubscription::new(rx_id, dbg!(class).to_string());
let glob = match WebhookGlob::from_str(dbg!(class)) {
Ok(glob) => glob,
Err(error) => panic!(
"event class glob {class:?} should produce the regex
{regex:?}, but instead failed to parse: {error}"
),
};
assert_eq!(
dbg!(regex),
dbg!(&subscription.similar_to),
dbg!(&glob.similar_to),
"event class {class:?} should produce the regex {regex:?}"
);
}
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ mod volume;
mod volume_repair;
mod vpc;
mod webhook_event;
mod webhook_rx;
mod zpool;

pub use address_lot::AddressLotCreateResult;
Expand Down
122 changes: 122 additions & 0 deletions nexus/db-queries/src/db/datastore/webhook_rx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! [`DataStore`] methods for webhook receiver management.
use super::DataStore;
use crate::authz;
use crate::context::OpContext;
use crate::db::collection_insert::AsyncInsertError;
use crate::db::collection_insert::DatastoreCollection;
use crate::db::error::public_error_from_diesel;
use crate::db::error::retryable;
use crate::db::error::ErrorHandler;
use crate::db::model::WebhookGlob;
use crate::db::model::WebhookReceiver;
use crate::db::model::WebhookRxSubscription;
use crate::db::pool::DbConnection;
use crate::db::TransactionError;
use async_bb8_diesel::AsyncConnection;
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::prelude::*;
use diesel::result::OptionalExtension;
use nexus_types::identity::Resource;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::LookupType;
use omicron_common::api::external::ResourceType;
use omicron_uuid_kinds::{GenericUuid, WebhookReceiverUuid};

impl DataStore {
pub async fn webhook_rx_create(
&self,
opctx: &OpContext,
receiver: &WebhookReceiver,
subscriptions: &[WebhookGlob],
) -> CreateResult<WebhookReceiver> {
use crate::db::schema::webhook_rx::dsl;
// TODO(eliza): someday we gotta allow creating webhooks with more
// restrictive permissions...
opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?;

let conn = self.pool_connection_authorized(opctx).await?;
let rx_id = WebhookReceiverUuid::from_untyped_uuid(receiver.id());
self.transaction_retry_wrapper("webhook_rx_create")
.transaction(&conn, |conn| {
let receiver = receiver.clone();
async move {
diesel::insert_into(dsl::webhook_rx)
.values(receiver)
// .on_conflict(dsl::id)
// .do_update()
// .set(dsl::time_modified.eq(dsl::time_modified))
// .returning(WebhookReceiver::as_returning())
.execute_async(&conn)
.await?;
// .map_err(|e| {
// if retryable(&e) {
// return TransactionError::Database(e);
// };
// TransactionError::CustomError(public_error_from_diesel(
// e,
// ErrorHandler::Conflict(
// ResourceType::WebhookReceiver,
// receiver.identity.name.as_str(),
// ),
// ))
// })?;
for glob in subscriptions {
match self
.webhook_add_subscription_on_conn(
WebhookRxSubscription::new(rx_id, glob.clone()),
&conn,
)
.await
{
Ok(_) => {}
Err(AsyncInsertError::CollectionNotFound) => {} // we just created it?
Err(AsyncInsertError::DatabaseError(e)) => {
return Err(e);
}
}
}
// TODO(eliza): secrets go here...
Ok(())
}
})
.await
.map_err(|e| {
public_error_from_diesel(
e,
ErrorHandler::Conflict(
ResourceType::WebhookReceiver,
receiver.name().as_str(),
),
)
})?;
Ok(receiver.clone())
}

async fn webhook_add_subscription_on_conn(
&self,
subscription: WebhookRxSubscription,
conn: &async_bb8_diesel::Connection<DbConnection>,
) -> Result<WebhookRxSubscription, AsyncInsertError> {
use crate::db::schema::webhook_rx_subscription::dsl;
let rx_id = subscription.rx_id.into_untyped_uuid();
let subscription: WebhookRxSubscription =
WebhookReceiver::insert_resource(
rx_id,
diesel::insert_into(dsl::webhook_rx_subscription)
.values(subscription)
.on_conflict((dsl::rx_id, dsl::event_class))
.do_update()
.set(dsl::time_created.eq(diesel::dsl::now)),
)
.insert_and_get_result_async(conn)
.await?;
Ok(subscription)
}
}
10 changes: 6 additions & 4 deletions schema/crdb/add-webhooks/up01.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx (
/* Identity metadata (resource) */
id UUID PRIMARY KEY,
-- A human-readable identifier for this webhook receiver.
name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ,
-- Child resource generation
rcgen INT NOT NULL,
-- URL of the endpoint webhooks are delivered to.
endpoint STRING(512) NOT NULL,
-- Whether or not liveness probes are sent to this receiver.
probes_enabled BOOL NOT NULL,
-- TODO(eliza): how do we track which roles are assigned to a webhook?
time_created TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ
);
11 changes: 6 additions & 5 deletions schema/crdb/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4693,18 +4693,19 @@ CREATE UNIQUE INDEX IF NOT EXISTS one_record_per_volume_resource_usage on omicro
*/

CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx (
/* Identity metadata (resource) */
id UUID PRIMARY KEY,
-- A human-readable identifier for this webhook receiver.
name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ,
-- Child resource generation
rcgen INT NOT NULL,
-- URL of the endpoint webhooks are delivered to.
endpoint STRING(512) NOT NULL,
-- Whether or not liveness probes are sent to this receiver.
probes_enabled BOOL NOT NULL,
-- TODO(eliza): how do we track which roles are assigned to a webhook?
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ,
time_deleted TIMESTAMPTZ
);

CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_secret (
Expand Down

0 comments on commit 0b80c8f

Please sign in to comment.