Skip to content

Commit 4118154

Browse files
committed
[nexus] Webhook API skeleton
This commit adds (unimplemented) public API endpoints for managing Nexus webhooks, as described in [RFD 364][1]. [1]: https://rfd.shared.oxide.computer/rfd/364#_external_api
1 parent dcc0df3 commit 4118154

File tree

9 files changed

+980
-13
lines changed

9 files changed

+980
-13
lines changed

Cargo.lock

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

nexus/external-api/output/nexus_tags.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ API operations found with tag "system/status"
231231
OPERATION ID METHOD URL PATH
232232
ping GET /v1/ping
233233

234+
API operations found with tag "system/webhooks"
235+
OPERATION ID METHOD URL PATH
236+
webhook_create POST /experimental/v1/webhooks
237+
webhook_delete DELETE /experimental/v1/webhooks/{webhook_id}
238+
webhook_delivery_list GET /experimental/v1/webhooks/{webhook_id}/deliveries
239+
webhook_delivery_resend POST /experimental/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/resend
240+
webhook_secrets_add POST /experimental/v1/webhooks/{webhook_id}/secrets
241+
webhook_secrets_list GET /experimental/v1/webhooks/{webhook_id}/secrets
242+
webhook_view GET /experimental/v1/webhooks/{webhook_id}
243+
234244
API operations found with tag "vpcs"
235245
OPERATION ID METHOD URL PATH
236246
internet_gateway_create POST /v1/internet-gateways

nexus/external-api/src/lib.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ pub const API_VERSION: &str = "20241204.0.0";
151151
url = "http://docs.oxide.computer/api/vpcs"
152152
}
153153
},
154+
"system/webhooks" = {
155+
description = "Webhooks deliver notifications for audit log events and fault management alerts.",
156+
external_docs = {
157+
url = "http://docs.oxide.computer/api/webhooks"
158+
}
159+
},
154160
"system/probes" = {
155161
description = "Probes for testing network connectivity",
156162
external_docs = {
@@ -3088,6 +3094,87 @@ pub trait NexusExternalApi {
30883094
rqctx: RequestContext<Self::Context>,
30893095
params: TypedBody<params::DeviceAccessTokenRequest>,
30903096
) -> Result<Response<Body>, HttpError>;
3097+
3098+
// Webhooks (experimental)
3099+
3100+
/// Get the configuration for a webhook.
3101+
#[endpoint {
3102+
method = GET,
3103+
path = "/experimental/v1/webhooks/{webhook_id}",
3104+
tags = ["system/webhooks"],
3105+
}]
3106+
async fn webhook_view(
3107+
rqctx: RequestContext<Self::Context>,
3108+
path_params: Path<params::WebhookPath>,
3109+
) -> Result<HttpResponseOk<views::Webhook>, HttpError>;
3110+
3111+
/// Create a new webhook receiver.
3112+
#[endpoint {
3113+
method = POST,
3114+
path = "/experimental/v1/webhooks",
3115+
tags = ["system/webhooks"],
3116+
}]
3117+
async fn webhook_create(
3118+
rqctx: RequestContext<Self::Context>,
3119+
params: TypedBody<params::WebhookCreate>,
3120+
) -> Result<HttpResponseCreated<views::Webhook>, HttpError>;
3121+
3122+
/// Delete a webhook receiver.
3123+
#[endpoint {
3124+
method = DELETE,
3125+
path = "/experimental/v1/webhooks/{webhook_id}",
3126+
tags = ["system/webhooks"],
3127+
}]
3128+
async fn webhook_delete(
3129+
rqctx: RequestContext<Self::Context>,
3130+
path_params: Path<params::WebhookPath>,
3131+
) -> Result<HttpResponseDeleted, HttpError>;
3132+
3133+
/// List the IDs of secrets for a webhook receiver.
3134+
#[endpoint {
3135+
method = GET,
3136+
path = "/experimental/v1/webhooks/{webhook_id}/secrets",
3137+
tags = ["system/webhooks"],
3138+
}]
3139+
async fn webhook_secrets_list(
3140+
rqctx: RequestContext<Self::Context>,
3141+
path_params: Path<params::WebhookPath>,
3142+
) -> Result<HttpResponseOk<views::WebhookSecrets>, HttpError>;
3143+
3144+
/// Add a secret to a webhook.
3145+
#[endpoint {
3146+
method = POST,
3147+
path = "/experimental/v1/webhooks/{webhook_id}/secrets",
3148+
tags = ["system/webhooks"],
3149+
}]
3150+
async fn webhook_secrets_add(
3151+
rqctx: RequestContext<Self::Context>,
3152+
path_params: Path<params::WebhookPath>,
3153+
params: TypedBody<params::WebhookSecret>,
3154+
) -> Result<HttpResponseCreated<views::WebhookSecretId>, HttpError>;
3155+
3156+
/// List delivery attempts to a webhook receiver.
3157+
#[endpoint {
3158+
method = GET,
3159+
path = "/experimental/v1/webhooks/{webhook_id}/deliveries",
3160+
tags = ["system/webhooks"],
3161+
}]
3162+
async fn webhook_delivery_list(
3163+
rqctx: RequestContext<Self::Context>,
3164+
path_params: Path<params::WebhookPath>,
3165+
query_params: Query<PaginatedById>,
3166+
) -> Result<HttpResponseOk<ResultsPage<views::WebhookDelivery>>, HttpError>;
3167+
3168+
/// Request re-delivery of a webhook event.
3169+
#[endpoint {
3170+
method = POST,
3171+
path = "/experimental/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/resend",
3172+
tags = ["system/webhooks"],
3173+
}]
3174+
async fn webhook_delivery_resend(
3175+
rqctx: RequestContext<Self::Context>,
3176+
path_params: Path<params::WebhookDeliveryPath>,
3177+
) -> Result<HttpResponseCreated<views::WebhookDeliveryId>, HttpError>;
30913178
}
30923179

30933180
/// Perform extra validations on the OpenAPI spec.

nexus/src/external_api/http_entrypoints.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6298,4 +6298,169 @@ impl NexusExternalApi for NexusExternalApiImpl {
62986298
) -> Result<Response<Body>, HttpError> {
62996299
device_auth::device_access_token(rqctx, params.into_inner()).await
63006300
}
6301+
6302+
async fn webhook_view(
6303+
rqctx: RequestContext<Self::Context>,
6304+
_path_params: Path<params::WebhookPath>,
6305+
) -> Result<HttpResponseOk<views::Webhook>, HttpError> {
6306+
let apictx = rqctx.context();
6307+
let handler = async {
6308+
let nexus = &apictx.context.nexus;
6309+
6310+
let opctx =
6311+
crate::context::op_context_for_external_api(&rqctx).await?;
6312+
6313+
Err(nexus
6314+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6315+
.await
6316+
.into())
6317+
};
6318+
apictx
6319+
.context
6320+
.external_latencies
6321+
.instrument_dropshot_handler(&rqctx, handler)
6322+
.await
6323+
}
6324+
6325+
async fn webhook_create(
6326+
rqctx: RequestContext<Self::Context>,
6327+
_params: TypedBody<params::WebhookCreate>,
6328+
) -> Result<HttpResponseCreated<views::Webhook>, HttpError> {
6329+
let apictx = rqctx.context();
6330+
let handler = async {
6331+
let nexus = &apictx.context.nexus;
6332+
6333+
let opctx =
6334+
crate::context::op_context_for_external_api(&rqctx).await?;
6335+
6336+
Err(nexus
6337+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6338+
.await
6339+
.into())
6340+
};
6341+
apictx
6342+
.context
6343+
.external_latencies
6344+
.instrument_dropshot_handler(&rqctx, handler)
6345+
.await
6346+
}
6347+
6348+
async fn webhook_delete(
6349+
rqctx: RequestContext<Self::Context>,
6350+
_path_params: Path<params::WebhookPath>,
6351+
) -> Result<HttpResponseDeleted, HttpError> {
6352+
let apictx = rqctx.context();
6353+
let handler = async {
6354+
let nexus = &apictx.context.nexus;
6355+
6356+
let opctx =
6357+
crate::context::op_context_for_external_api(&rqctx).await?;
6358+
6359+
Err(nexus
6360+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6361+
.await
6362+
.into())
6363+
};
6364+
apictx
6365+
.context
6366+
.external_latencies
6367+
.instrument_dropshot_handler(&rqctx, handler)
6368+
.await
6369+
}
6370+
6371+
async fn webhook_secrets_list(
6372+
rqctx: RequestContext<Self::Context>,
6373+
_path_params: Path<params::WebhookPath>,
6374+
) -> Result<HttpResponseOk<views::WebhookSecrets>, HttpError> {
6375+
let apictx = rqctx.context();
6376+
let handler = async {
6377+
let nexus = &apictx.context.nexus;
6378+
6379+
let opctx =
6380+
crate::context::op_context_for_external_api(&rqctx).await?;
6381+
6382+
Err(nexus
6383+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6384+
.await
6385+
.into())
6386+
};
6387+
apictx
6388+
.context
6389+
.external_latencies
6390+
.instrument_dropshot_handler(&rqctx, handler)
6391+
.await
6392+
}
6393+
6394+
/// Add a secret to a webhook.
6395+
async fn webhook_secrets_add(
6396+
rqctx: RequestContext<Self::Context>,
6397+
_path_params: Path<params::WebhookPath>,
6398+
_params: TypedBody<params::WebhookSecret>,
6399+
) -> Result<HttpResponseCreated<views::WebhookSecretId>, HttpError> {
6400+
let apictx = rqctx.context();
6401+
let handler = async {
6402+
let nexus = &apictx.context.nexus;
6403+
6404+
let opctx =
6405+
crate::context::op_context_for_external_api(&rqctx).await?;
6406+
6407+
Err(nexus
6408+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6409+
.await
6410+
.into())
6411+
};
6412+
apictx
6413+
.context
6414+
.external_latencies
6415+
.instrument_dropshot_handler(&rqctx, handler)
6416+
.await
6417+
}
6418+
6419+
async fn webhook_delivery_list(
6420+
rqctx: RequestContext<Self::Context>,
6421+
_path_params: Path<params::WebhookPath>,
6422+
_query_params: Query<PaginatedById>,
6423+
) -> Result<HttpResponseOk<ResultsPage<views::WebhookDelivery>>, HttpError>
6424+
{
6425+
let apictx = rqctx.context();
6426+
let handler = async {
6427+
let nexus = &apictx.context.nexus;
6428+
6429+
let opctx =
6430+
crate::context::op_context_for_external_api(&rqctx).await?;
6431+
6432+
Err(nexus
6433+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6434+
.await
6435+
.into())
6436+
};
6437+
apictx
6438+
.context
6439+
.external_latencies
6440+
.instrument_dropshot_handler(&rqctx, handler)
6441+
.await
6442+
}
6443+
6444+
async fn webhook_delivery_resend(
6445+
rqctx: RequestContext<Self::Context>,
6446+
_path_params: Path<params::WebhookDeliveryPath>,
6447+
) -> Result<HttpResponseCreated<views::WebhookDeliveryId>, HttpError> {
6448+
let apictx = rqctx.context();
6449+
let handler = async {
6450+
let nexus = &apictx.context.nexus;
6451+
6452+
let opctx =
6453+
crate::context::op_context_for_external_api(&rqctx).await?;
6454+
6455+
Err(nexus
6456+
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
6457+
.await
6458+
.into())
6459+
};
6460+
apictx
6461+
.context
6462+
.external_latencies
6463+
.instrument_dropshot_handler(&rqctx, handler)
6464+
.await
6465+
}
63016466
}

nexus/types/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ openssl.workspace = true
2929
oxql-types.workspace = true
3030
oxnet.workspace = true
3131
parse-display.workspace = true
32-
schemars = { workspace = true, features = ["chrono", "uuid1"] }
32+
schemars = { workspace = true, features = ["chrono", "uuid1", "url"] }
3333
serde.workspace = true
3434
serde_json.workspace = true
3535
serde_with.workspace = true
@@ -41,6 +41,7 @@ thiserror.workspace = true
4141
newtype-uuid.workspace = true
4242
update-engine.workspace = true
4343
uuid.workspace = true
44+
url = { workspace = true, features = ["serde"] }
4445

4546
api_identity.workspace = true
4647
gateway-client.workspace = true

nexus/types/src/external_api/params.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use std::collections::BTreeMap;
2828
use std::collections::BTreeSet;
2929
use std::collections::HashMap;
3030
use std::{net::IpAddr, str::FromStr};
31+
use url::Url;
3132
use uuid::Uuid;
3233

3334
macro_rules! path_param {
@@ -91,6 +92,7 @@ path_param!(ProbePath, probe, "probe");
9192
path_param!(CertificatePath, certificate, "certificate");
9293

9394
id_path_param!(GroupPath, group_id, "group");
95+
id_path_param!(WebhookPath, webhook_id, "webhook");
9496

9597
// TODO: The hardware resources should be represented by its UUID or a hardware
9698
// ID that can be used to deterministically generate the UUID.
@@ -2277,3 +2279,24 @@ pub struct DeviceAccessTokenRequest {
22772279
pub device_code: String,
22782280
pub client_id: Uuid,
22792281
}
2282+
2283+
// Webhooks
2284+
2285+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
2286+
pub struct WebhookCreate {
2287+
pub name: String,
2288+
pub endpoint: Url,
2289+
pub secrets: Vec<String>,
2290+
pub events: Vec<String>,
2291+
}
2292+
2293+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
2294+
pub struct WebhookSecret {
2295+
pub secret: String,
2296+
}
2297+
2298+
#[derive(Deserialize, JsonSchema)]
2299+
pub struct WebhookDeliveryPath {
2300+
pub webhook_id: Uuid,
2301+
pub delivery_id: Uuid,
2302+
}

0 commit comments

Comments
 (0)