From 3aee3cd921c64fdc096d379f24442e6f4abb62e4 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Thu, 2 Jan 2025 12:28:03 -0800 Subject: [PATCH] update to match RFD 538 --- nexus/external-api/output/nexus_tags.txt | 3 +- nexus/external-api/src/lib.rs | 14 ++- nexus/src/external_api/http_entrypoints.rs | 24 ++++ nexus/types/src/external_api/params.rs | 35 +++++- nexus/types/src/external_api/views.rs | 18 ++- openapi/nexus.json | 123 +++++++++++++++++++-- 6 files changed, 203 insertions(+), 14 deletions(-) diff --git a/nexus/external-api/output/nexus_tags.txt b/nexus/external-api/output/nexus_tags.txt index 0b97958e11..8a403fe09f 100644 --- a/nexus/external-api/output/nexus_tags.txt +++ b/nexus/external-api/output/nexus_tags.txt @@ -236,9 +236,10 @@ 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_delivery_resend POST /experimental/v1/webhooks/{webhook_id}/deliveries/{event_id}/resend webhook_secrets_add POST /experimental/v1/webhooks/{webhook_id}/secrets webhook_secrets_list GET /experimental/v1/webhooks/{webhook_id}/secrets +webhook_update PUT /experimental/v1/webhooks/{webhook_id} webhook_view GET /experimental/v1/webhooks/{webhook_id} API operations found with tag "vpcs" diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index a39f379240..e8cf0fbd90 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -3119,6 +3119,18 @@ pub trait NexusExternalApi { params: TypedBody, ) -> Result, HttpError>; + /// Update the configuration of an existing webhook receiver. + #[endpoint { + method = PUT, + path = "/experimental/v1/webhooks/{webhook_id}", + tags = ["system/webhooks"], + }] + async fn webhook_update( + rqctx: RequestContext, + path_params: Path, + params: TypedBody, + ) -> Result; + /// Delete a webhook receiver. #[endpoint { method = DELETE, @@ -3168,7 +3180,7 @@ pub trait NexusExternalApi { /// Request re-delivery of a webhook event. #[endpoint { method = POST, - path = "/experimental/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/resend", + path = "/experimental/v1/webhooks/{webhook_id}/deliveries/{event_id}/resend", tags = ["system/webhooks"], }] async fn webhook_delivery_resend( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index cb15f4d8fb..3164c492dc 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -6345,6 +6345,30 @@ impl NexusExternalApi for NexusExternalApiImpl { .await } + async fn webhook_update( + rqctx: RequestContext, + _path_params: Path, + _params: TypedBody, + ) -> Result { + 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, _path_params: Path, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 53650ac875..d3a7fc5c15 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -2284,10 +2284,43 @@ pub struct DeviceAccessTokenRequest { #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct WebhookCreate { + /// An identifier for this webhook receiver, which must be unique. pub name: String, + + /// The URL that webhook notification requests should be sent to pub endpoint: Url, + + /// A non-empty list of secret keys used to sign webhook payloads. pub secrets: Vec, + + /// A list of webhook event classes to subscribe to. + /// + /// If this list is empty or is not included in the request body, the + /// webhook will not be subscribed to any events. + #[serde(default)] pub events: Vec, + + /// If `true`, liveness probe requests will not be sent to this webhook receiver. + #[serde(default)] + pub disable_probes: bool, +} + +/// Parameters to update a webhook configuration. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct WebhookUpdate { + /// An identifier for this webhook receiver, which must be unique. + pub name: String, + + /// The URL that webhook notification requests should be sent to + pub endpoint: Url, + + /// A list of webhook event classes to subscribe to. + /// + /// If this list is empty, the webhook will not be subscribed to any events. + pub events: Vec, + + /// If `true`, liveness probe requests will not be sent to this webhook receiver. + pub disable_probes: bool, } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] @@ -2298,5 +2331,5 @@ pub struct WebhookSecret { #[derive(Deserialize, JsonSchema)] pub struct WebhookDeliveryPath { pub webhook_id: Uuid, - pub delivery_id: Uuid, + pub event_id: Uuid, } diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 59888a4e9b..1112bf0a6d 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -1033,13 +1033,22 @@ pub struct OxqlQueryResult { /// The configuration for a webhook. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct Webhook { + /// The UUID of this webhook receiver. pub id: WebhookUuid, + /// The identifier assigned to this webhook receiver upon creation. pub name: String, + /// The URL that webhook notification requests are sent to. pub endpoint: Url, + /// The UUID of the user associated with this webhook receiver for + /// role-based ccess control. + pub actor_id: Uuid, + // A list containing the IDs of the secret keys used to sign payloads sent + // to this receiver. pub secrets: Vec, - // XXX(eliza): should eventually be an enum? + /// The list of event classes to which this receiver is subscribed. pub events: Vec, - // TODO(eliza): roles? + /// If `true`, liveness probe requests are not sent to this receiver. + pub disable_probes: bool, } /// A list of the IDs of secrets associated with a webhook. @@ -1065,7 +1074,7 @@ pub struct WebhookDelivery { pub webhook_id: WebhookUuid, /// The event class. - pub event: String, + pub event_class: String, /// The UUID of the event. pub event_id: EventUuid, @@ -1105,6 +1114,9 @@ pub enum WebhookDeliveryState { FailedHttpError, /// The webhook request could not be sent to the receiver endpoint. FailedUnreachable, + /// A connection to the receiver endpoint was successfully established, but + /// no response was received within the delivery timeout. + FailedTimeout, } /// The response received from a webhook receiver endpoint. diff --git a/openapi/nexus.json b/openapi/nexus.json index 6f8ba8b2a0..ff43ed9a0e 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -343,7 +343,7 @@ "tags": [ "system/webhooks" ], - "summary": "Get the configuration for a webhook.", + "summary": "Get the configuration for a webhook receiver.", "operationId": "webhook_view", "parameters": [ { @@ -376,6 +376,46 @@ } } }, + "put": { + "tags": [ + "system/webhooks" + ], + "summary": "Update the configuration of an existing webhook receiver.", + "operationId": "webhook_update", + "parameters": [ + { + "in": "path", + "name": "webhook_id", + "description": "ID of the webhook", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, "delete": { "tags": [ "system/webhooks" @@ -476,7 +516,7 @@ } } }, - "/experimental/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/resend": { + "/experimental/v1/webhooks/{webhook_id}/deliveries/{event_id}/resend": { "post": { "tags": [ "system/webhooks" @@ -486,7 +526,7 @@ "parameters": [ { "in": "path", - "name": "delivery_id", + "name": "event_id", "required": true, "schema": { "type": "string", @@ -565,7 +605,7 @@ "tags": [ "system/webhooks" ], - "summary": "Add a secret to a webhook.", + "summary": "Add a secret to a webhook receiver.", "operationId": "webhook_secrets_add", "parameters": [ { @@ -22803,20 +22843,37 @@ "description": "The configuration for a webhook.", "type": "object", "properties": { + "actor_id": { + "description": "The UUID of the user associated with this webhook receiver for role-based ccess control.", + "type": "string", + "format": "uuid" + }, + "disable_probes": { + "description": "If `true`, liveness probe requests are not sent to this receiver.", + "type": "boolean" + }, "endpoint": { + "description": "The URL that webhook notification requests are sent to.", "type": "string", "format": "uri" }, "events": { + "description": "The list of event classes to which this receiver is subscribed.", "type": "array", "items": { "type": "string" } }, "id": { - "$ref": "#/components/schemas/TypedUuidForWebhookKind" + "description": "The UUID of this webhook receiver.", + "allOf": [ + { + "$ref": "#/components/schemas/TypedUuidForWebhookKind" + } + ] }, "name": { + "description": "The identifier assigned to this webhook receiver upon creation.", "type": "string" }, "secrets": { @@ -22827,6 +22884,8 @@ } }, "required": [ + "actor_id", + "disable_probes", "endpoint", "events", "id", @@ -22837,20 +22896,30 @@ "WebhookCreate": { "type": "object", "properties": { + "disable_probes": { + "description": "If `true`, liveness probe requests will not be sent to this webhook receiver.", + "default": false, + "type": "boolean" + }, "endpoint": { + "description": "The URL that webhook notification requests should be sent to", "type": "string", "format": "uri" }, "events": { + "description": "A list of webhook event classes to subscribe to.\n\nIf this list is empty or is not included in the request body, the webhook will not be subscribed to any events.", + "default": [], "type": "array", "items": { "type": "string" } }, "name": { + "description": "An identifier for this webhook receiver, which must be unique.", "type": "string" }, "secrets": { + "description": "A non-empty list of secret keys used to sign webhook payloads.", "type": "array", "items": { "type": "string" @@ -22859,7 +22928,6 @@ }, "required": [ "endpoint", - "events", "name", "secrets" ] @@ -22868,7 +22936,7 @@ "description": "A delivery attempt for a webhook event.", "type": "object", "properties": { - "event": { + "event_class": { "description": "The event class.", "type": "string" }, @@ -22924,7 +22992,7 @@ } }, "required": [ - "event", + "event_class", "event_id", "id", "state", @@ -23016,6 +23084,13 @@ "enum": [ "failed_unreachable" ] + }, + { + "description": "A connection to the receiver endpoint was successfully established, but no response was received within the delivery timeout.", + "type": "string", + "enum": [ + "failed_timeout" + ] } ] }, @@ -23057,6 +23132,38 @@ "secrets" ] }, + "WebhookUpdate": { + "description": "Parameters to update a webhook configuration.", + "type": "object", + "properties": { + "disable_probes": { + "description": "If `true`, liveness probe requests will not be sent to this webhook receiver.", + "type": "boolean" + }, + "endpoint": { + "description": "The URL that webhook notification requests should be sent to", + "type": "string", + "format": "uri" + }, + "events": { + "description": "A list of webhook event classes to subscribe to.\n\nIf this list is empty, the webhook will not be subscribed to any events.", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "An identifier for this webhook receiver, which must be unique.", + "type": "string" + } + }, + "required": [ + "disable_probes", + "endpoint", + "events", + "name" + ] + }, "NameOrIdSortMode": { "description": "Supported set of sort modes for scanning by name or id", "oneOf": [