Skip to content

Commit

Permalink
[nexus] Webhook API skeleton
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hawkw committed Dec 18, 2024
1 parent dcc0df3 commit 4118154
Show file tree
Hide file tree
Showing 9 changed files with 980 additions and 13 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@ API operations found with tag "system/status"
OPERATION ID METHOD URL PATH
ping GET /v1/ping

API operations found with tag "system/webhooks"
OPERATION ID METHOD URL PATH
webhook_create POST /experimental/v1/webhooks
webhook_delete DELETE /experimental/v1/webhooks/{webhook_id}
webhook_delivery_list GET /experimental/v1/webhooks/{webhook_id}/deliveries
webhook_delivery_resend POST /experimental/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/resend
webhook_secrets_add POST /experimental/v1/webhooks/{webhook_id}/secrets
webhook_secrets_list GET /experimental/v1/webhooks/{webhook_id}/secrets
webhook_view GET /experimental/v1/webhooks/{webhook_id}

API operations found with tag "vpcs"
OPERATION ID METHOD URL PATH
internet_gateway_create POST /v1/internet-gateways
Expand Down
87 changes: 87 additions & 0 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ pub const API_VERSION: &str = "20241204.0.0";
url = "http://docs.oxide.computer/api/vpcs"
}
},
"system/webhooks" = {
description = "Webhooks deliver notifications for audit log events and fault management alerts.",
external_docs = {
url = "http://docs.oxide.computer/api/webhooks"
}
},
"system/probes" = {
description = "Probes for testing network connectivity",
external_docs = {
Expand Down Expand Up @@ -3088,6 +3094,87 @@ pub trait NexusExternalApi {
rqctx: RequestContext<Self::Context>,
params: TypedBody<params::DeviceAccessTokenRequest>,
) -> Result<Response<Body>, HttpError>;

// Webhooks (experimental)

/// Get the configuration for a webhook.
#[endpoint {
method = GET,
path = "/experimental/v1/webhooks/{webhook_id}",
tags = ["system/webhooks"],
}]
async fn webhook_view(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::WebhookPath>,
) -> Result<HttpResponseOk<views::Webhook>, HttpError>;

/// Create a new webhook receiver.
#[endpoint {
method = POST,
path = "/experimental/v1/webhooks",
tags = ["system/webhooks"],
}]
async fn webhook_create(
rqctx: RequestContext<Self::Context>,
params: TypedBody<params::WebhookCreate>,
) -> Result<HttpResponseCreated<views::Webhook>, HttpError>;

/// Delete a webhook receiver.
#[endpoint {
method = DELETE,
path = "/experimental/v1/webhooks/{webhook_id}",
tags = ["system/webhooks"],
}]
async fn webhook_delete(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::WebhookPath>,
) -> Result<HttpResponseDeleted, HttpError>;

/// List the IDs of secrets for a webhook receiver.
#[endpoint {
method = GET,
path = "/experimental/v1/webhooks/{webhook_id}/secrets",
tags = ["system/webhooks"],
}]
async fn webhook_secrets_list(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::WebhookPath>,
) -> Result<HttpResponseOk<views::WebhookSecrets>, HttpError>;

/// Add a secret to a webhook.
#[endpoint {
method = POST,
path = "/experimental/v1/webhooks/{webhook_id}/secrets",
tags = ["system/webhooks"],
}]
async fn webhook_secrets_add(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::WebhookPath>,
params: TypedBody<params::WebhookSecret>,
) -> Result<HttpResponseCreated<views::WebhookSecretId>, HttpError>;

/// List delivery attempts to a webhook receiver.
#[endpoint {
method = GET,
path = "/experimental/v1/webhooks/{webhook_id}/deliveries",
tags = ["system/webhooks"],
}]
async fn webhook_delivery_list(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::WebhookPath>,
query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<views::WebhookDelivery>>, HttpError>;

/// Request re-delivery of a webhook event.
#[endpoint {
method = POST,
path = "/experimental/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/resend",
tags = ["system/webhooks"],
}]
async fn webhook_delivery_resend(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::WebhookDeliveryPath>,
) -> Result<HttpResponseCreated<views::WebhookDeliveryId>, HttpError>;
}

/// Perform extra validations on the OpenAPI spec.
Expand Down
165 changes: 165 additions & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6298,4 +6298,169 @@ impl NexusExternalApi for NexusExternalApiImpl {
) -> Result<Response<Body>, HttpError> {
device_auth::device_access_token(rqctx, params.into_inner()).await
}

async fn webhook_view(
rqctx: RequestContext<Self::Context>,
_path_params: Path<params::WebhookPath>,
) -> Result<HttpResponseOk<views::Webhook>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn webhook_create(
rqctx: RequestContext<Self::Context>,
_params: TypedBody<params::WebhookCreate>,
) -> Result<HttpResponseCreated<views::Webhook>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn webhook_delete(
rqctx: RequestContext<Self::Context>,
_path_params: Path<params::WebhookPath>,
) -> Result<HttpResponseDeleted, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn webhook_secrets_list(
rqctx: RequestContext<Self::Context>,
_path_params: Path<params::WebhookPath>,
) -> Result<HttpResponseOk<views::WebhookSecrets>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

/// Add a secret to a webhook.
async fn webhook_secrets_add(
rqctx: RequestContext<Self::Context>,
_path_params: Path<params::WebhookPath>,
_params: TypedBody<params::WebhookSecret>,
) -> Result<HttpResponseCreated<views::WebhookSecretId>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn webhook_delivery_list(
rqctx: RequestContext<Self::Context>,
_path_params: Path<params::WebhookPath>,
_query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<views::WebhookDelivery>>, HttpError>
{
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn webhook_delivery_resend(
rqctx: RequestContext<Self::Context>,
_path_params: Path<params::WebhookDeliveryPath>,
) -> Result<HttpResponseCreated<views::WebhookDeliveryId>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;

let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;

Err(nexus
.unimplemented_todo(&opctx, crate::app::Unimpl::Public)
.await
.into())
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}
}
3 changes: 2 additions & 1 deletion nexus/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ openssl.workspace = true
oxql-types.workspace = true
oxnet.workspace = true
parse-display.workspace = true
schemars = { workspace = true, features = ["chrono", "uuid1"] }
schemars = { workspace = true, features = ["chrono", "uuid1", "url"] }
serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
Expand All @@ -41,6 +41,7 @@ thiserror.workspace = true
newtype-uuid.workspace = true
update-engine.workspace = true
uuid.workspace = true
url = { workspace = true, features = ["serde"] }

api_identity.workspace = true
gateway-client.workspace = true
Expand Down
23 changes: 23 additions & 0 deletions nexus/types/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::{net::IpAddr, str::FromStr};
use url::Url;
use uuid::Uuid;

macro_rules! path_param {
Expand Down Expand Up @@ -91,6 +92,7 @@ path_param!(ProbePath, probe, "probe");
path_param!(CertificatePath, certificate, "certificate");

id_path_param!(GroupPath, group_id, "group");
id_path_param!(WebhookPath, webhook_id, "webhook");

// TODO: The hardware resources should be represented by its UUID or a hardware
// ID that can be used to deterministically generate the UUID.
Expand Down Expand Up @@ -2277,3 +2279,24 @@ pub struct DeviceAccessTokenRequest {
pub device_code: String,
pub client_id: Uuid,
}

// Webhooks

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct WebhookCreate {
pub name: String,
pub endpoint: Url,
pub secrets: Vec<String>,
pub events: Vec<String>,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct WebhookSecret {
pub secret: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct WebhookDeliveryPath {
pub webhook_id: Uuid,
pub delivery_id: Uuid,
}
Loading

0 comments on commit 4118154

Please sign in to comment.