diff --git a/.spectral.yaml b/.spectral.yaml index 05f2c00..60d3916 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -2,6 +2,6 @@ extends: ["spectral:oas"] overrides: - files: - "schemas/core/openapi.yaml#/components/schemas/BearerAuth" - - "schemas/gcp/openapi.yaml#/components/schemas/BearerAuth" + - "schemas/core/openapi.yaml#/components/schemas/ForbiddenResponse" rules: oas3-unused-component: off diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab4d7b..cba7b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.20] - 2026-06-02 + +### Fixed + +- Typed error response models (`BadRequestResponse`, `UnauthorizedResponse`, `NotFoundResponse`, `ConflictResponse`) now emit an `application/problem+json` body schema per RFC 9457 (HYPERFLEET-993) +- `UnauthorizedResponse` (401) added to all status and force-delete endpoints +- `ConflictResponse` (409) removed from status create/update endpoints (upsert semantics make conflict impossible) +- Default `Error` response added to status create/update endpoints where it was previously missing + +### Changed + +- `page` and `pageSize` query parameters have `@minValue(1)` constraints (HYPERFLEET-993) +- Error models scoped to `namespace HyperFleet {}` block to avoid collision with TypeSpec.Http built-ins +- Error example constants extracted to `shared/models/common/example_errors.tsp` + ## [1.0.18] - 2026-05-26 ### Changed @@ -179,7 +194,8 @@ First official stable release of the HyperFleet API specification. - Interactive API documentation -[Unreleased]: https://github.com/openshift-hyperfleet/hyperfleet-api-spec/compare/v1.0.18...HEAD +[Unreleased]: https://github.com/openshift-hyperfleet/hyperfleet-api-spec/compare/v1.0.20...HEAD +[1.0.20]: https://github.com/openshift-hyperfleet/hyperfleet-api-spec/compare/v1.0.18...v1.0.20 [1.0.18]: https://github.com/openshift-hyperfleet/hyperfleet-api-spec/compare/v1.0.17...v1.0.18 [1.0.17]: https://github.com/openshift-hyperfleet/hyperfleet-api-spec/compare/v1.0.16...v1.0.17 [1.0.16]: https://github.com/openshift-hyperfleet/hyperfleet-api-spec/compare/v1.0.15...v1.0.16 diff --git a/core/services/force-delete-internal.tsp b/core/services/force-delete-internal.tsp index 5925265..8f93d24 100644 --- a/core/services/force-delete-internal.tsp +++ b/core/services/force-delete-internal.tsp @@ -28,8 +28,9 @@ interface ClustersForceDelete { ): { @statusCode statusCode: 204; } | Error - | NotFoundResponse | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; } @@ -54,7 +55,8 @@ interface NodePoolsForceDelete { ): { @statusCode statusCode: 204; } | Error - | NotFoundResponse | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; } diff --git a/core/services/resources-internal.tsp b/core/services/resources-internal.tsp index c2810d0..2f5a85e 100644 --- a/core/services/resources-internal.tsp +++ b/core/services/resources-internal.tsp @@ -24,7 +24,8 @@ interface Resources { @operationId("getResources") getResources(...QueryParams): Body | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse; /** * Returns a single resource by its ID. @@ -38,8 +39,9 @@ interface Resources { @path resource_id: string, ): Resource | Error - | NotFoundResponse - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; /** * Create a new resource. Only top-level entity types (no parent) can be created @@ -53,7 +55,8 @@ interface Resources { @statusCode statusCode: 201; @body resource: Resource; } | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse; /** * Patch a resource by ID. Supports partial updates to spec, labels, and references. @@ -67,8 +70,9 @@ interface Resources { @body body: ResourcePatchRequest, ): Resource | Error - | NotFoundResponse | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; /** @@ -85,10 +89,11 @@ interface Resources { ): { @statusCode statusCode: 202; @body resource: Resource; - } | NotFoundResponse - | ConflictResponse - | Error - | BadRequestResponse; + } | Error + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse + | ConflictResponse; /** * Permanently removes the resource record from the database for a resource stuck in Finalizing state. @@ -104,8 +109,9 @@ interface Resources { ): { @statusCode statusCode: 204; } | Error - | NotFoundResponse | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; } @@ -125,8 +131,9 @@ interface ResourceStatuses { ...QueryParams, ): Body | Error - | NotFoundResponse - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; @route("") @put @@ -138,7 +145,8 @@ interface ResourceStatuses { @body body: AdapterStatusCreateRequest, ): | (CreatedResponse & AdapterStatus) + | Error | BadRequestResponse - | NotFoundResponse - | ConflictResponse; + | UnauthorizedResponse + | NotFoundResponse; } diff --git a/core/services/statuses-internal.tsp b/core/services/statuses-internal.tsp index d180702..b4b6968 100644 --- a/core/services/statuses-internal.tsp +++ b/core/services/statuses-internal.tsp @@ -30,8 +30,9 @@ interface ClusterStatuses { ...QueryParams ): Body | Error - | NotFoundResponse - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; @route("") @put @@ -47,9 +48,10 @@ interface ClusterStatuses { @body body: AdapterStatusCreateRequest, ): | (CreatedResponse & AdapterStatus) + | Error | BadRequestResponse - | NotFoundResponse - | ConflictResponse; + | UnauthorizedResponse + | NotFoundResponse; } @tag("NodePool statuses") @@ -72,8 +74,9 @@ interface NodePoolStatuses { ...QueryParams ): Body | Error - | NotFoundResponse - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; @route("") @put @@ -94,7 +97,9 @@ interface NodePoolStatuses { @body body: AdapterStatusCreateRequest, ): | (CreatedResponse & AdapterStatus) + | Error | BadRequestResponse - | NotFoundResponse - | ConflictResponse; + | UnauthorizedResponse + | NotFoundResponse; } + diff --git a/main.tsp b/main.tsp index f5f2e99..25e8650 100644 --- a/main.tsp +++ b/main.tsp @@ -30,7 +30,7 @@ using OpenAPI; */ @service(#{ title: "HyperFleet API" }) @info(#{ - version: "1.0.19", + version: "1.0.20", contact: #{ name: "HyperFleet Team", url: "https://github.com/openshift-hyperfleet", diff --git a/package-lock.json b/package-lock.json index 00b0888..31085cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hyperfleet", - "version": "1.0.19", + "version": "1.0.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hyperfleet", - "version": "1.0.19", + "version": "1.0.20", "devDependencies": { "@stoplight/spectral-cli": "6.15.1", "@typespec/compiler": "^1.6.0", diff --git a/package.json b/package.json index 66e86ec..c275f09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyperfleet", - "version": "1.0.19", + "version": "1.0.20", "type": "module", "exports": { "./*": "./*" diff --git a/schemas/core/openapi.yaml b/schemas/core/openapi.yaml index 8c9a92f..18bc484 100644 --- a/schemas/core/openapi.yaml +++ b/schemas/core/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: HyperFleet API - version: 1.0.19 + version: 1.0.20 contact: name: HyperFleet Team url: https://github.com/openshift-hyperfleet @@ -42,12 +42,22 @@ paths: $ref: '#/components/schemas/ClusterList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Clusters security: @@ -71,12 +81,28 @@ paths: $ref: '#/components/schemas/Cluster' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' + '409': + description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Clusters requestBody: @@ -108,12 +134,28 @@ paths: $ref: '#/components/schemas/Cluster' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' + '404': + description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Clusters security: @@ -138,16 +180,34 @@ paths: $ref: '#/components/schemas/Cluster' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Clusters requestBody: @@ -230,14 +290,28 @@ paths: deleted_by: user-123@example.com '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Clusters security: @@ -261,16 +335,34 @@ paths: description: 'There is no content to send for this request, but the headers may be useful. ' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Clusters requestBody: @@ -307,12 +399,22 @@ paths: $ref: '#/components/schemas/NodePoolList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools security: @@ -337,14 +439,34 @@ paths: $ref: '#/components/schemas/NodePoolCreateResponse' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' + '404': + description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools requestBody: @@ -382,12 +504,28 @@ paths: $ref: '#/components/schemas/NodePool' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' + '404': + description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools security: @@ -474,14 +612,28 @@ paths: deleted_by: user-123@example.com '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools security: @@ -512,16 +664,34 @@ paths: $ref: '#/components/schemas/NodePool' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools requestBody: @@ -557,16 +727,34 @@ paths: description: 'There is no content to send for this request, but the headers may be useful. ' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools requestBody: @@ -608,14 +796,28 @@ paths: $ref: '#/components/schemas/AdapterStatusList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePool statuses security: @@ -646,10 +848,28 @@ paths: $ref: '#/components/schemas/AdapterStatus' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' + default: + description: An unexpected error response. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' tags: - NodePool statuses requestBody: @@ -686,14 +906,28 @@ paths: $ref: '#/components/schemas/AdapterStatusList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Cluster statuses security: @@ -718,10 +952,28 @@ paths: $ref: '#/components/schemas/AdapterStatus' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' + default: + description: An unexpected error response. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' tags: - Cluster statuses requestBody: @@ -752,12 +1004,22 @@ paths: $ref: '#/components/schemas/NodePoolList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - NodePools security: @@ -782,12 +1044,22 @@ paths: $ref: '#/components/schemas/ResourceList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resources security: @@ -808,12 +1080,22 @@ paths: $ref: '#/components/schemas/Resource' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resources requestBody: @@ -845,14 +1127,28 @@ paths: $ref: '#/components/schemas/Resource' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resources security: @@ -876,16 +1172,34 @@ paths: $ref: '#/components/schemas/Resource' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resources requestBody: @@ -918,16 +1232,34 @@ paths: $ref: '#/components/schemas/Resource' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resources security: @@ -950,16 +1282,34 @@ paths: description: 'There is no content to send for this request, but the headers may be useful. ' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' '409': description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ConflictDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resources requestBody: @@ -995,14 +1345,28 @@ paths: $ref: '#/components/schemas/AdapterStatusList' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' default: description: An unexpected error response. content: application/problem+json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/ProblemDetails' tags: - Resource statuses security: @@ -1026,10 +1390,28 @@ paths: $ref: '#/components/schemas/AdapterStatus' '400': description: The server could not understand the request due to invalid syntax. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/BadRequestDetails' + '401': + description: The request requires valid authentication credentials. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/UnauthorizedDetails' '404': description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/NotFoundDetails' + default: + description: An unexpected error response. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' tags: - Resource statuses requestBody: @@ -1064,6 +1446,7 @@ components: schema: type: integer format: int32 + minimum: 1 default: 1 explode: false QueryParams.pageSize: @@ -1073,6 +1456,7 @@ components: schema: type: integer format: int32 + minimum: 1 default: 20 explode: false SearchParams: @@ -1356,6 +1740,19 @@ components: last_transition_time: '2021-01-01T10:01:00Z' created_time: '2021-01-01T10:01:00Z' last_report_time: '2021-01-01T10:01:30Z' + BadRequestDetails: + type: object + allOf: + - $ref: '#/components/schemas/ProblemDetails' + example: + type: https://api.hyperfleet.io/errors/validation-error + title: Validation Failed + status: 400 + detail: The cluster name field is required + instance: /api/hyperfleet/v1/clusters + code: HYPERFLEET-VAL-001 + timestamp: '2024-01-15T10:30:00Z' + trace_id: aabbccddeeff0011 BearerAuth: type: object required: @@ -1605,54 +2002,40 @@ components: description: |- Condition data for create/update requests (from adapters) observed_generation and observed_time are now at AdapterStatusCreateRequest level - Error: + ConflictDetails: + type: object + allOf: + - $ref: '#/components/schemas/ProblemDetails' + example: + type: https://api.hyperfleet.io/errors/resource-conflict + title: Resource Conflict + status: 409 + detail: This Cluster already exists + instance: /api/hyperfleet/v1/clusters + code: HYPERFLEET-CNF-001 + timestamp: '2024-01-15T10:30:00Z' + trace_id: cafe0011aabb9988 + ForbiddenDetails: + type: object + allOf: + - $ref: '#/components/schemas/ProblemDetails' + example: + type: https://api.hyperfleet.io/errors/permission-denied + title: Permission Denied + status: 403 + detail: You do not have permission to perform this action + instance: /api/hyperfleet/v1/clusters + code: HYPERFLEET-AUZ-001 + timestamp: '2024-01-15T10:30:00Z' + trace_id: 99aabbcc11223344 + ForbiddenResponse: type: object required: - - type - - title - - status + - body properties: - type: - type: string - format: uri - description: URI reference identifying the problem type - example: https://api.hyperfleet.io/errors/validation-error - title: - type: string - description: Short human-readable summary of the problem - example: Validation Failed - status: - type: integer - description: HTTP status code - example: 400 - detail: - type: string - description: Human-readable explanation specific to this occurrence - example: The cluster name field is required - instance: - type: string - format: uri-reference - description: URI reference for this specific occurrence - example: /api/hyperfleet/v1/clusters - code: - type: string - description: Machine-readable error code in HYPERFLEET-CAT-NUM format - example: HYPERFLEET-VAL-001 - timestamp: - type: string - format: date-time - description: RFC3339 timestamp of when the error occurred - example: '2024-01-15T10:30:00Z' - trace_id: - type: string - description: Distributed trace ID for correlation - example: abc123def456 - errors: - type: array - items: - $ref: '#/components/schemas/ValidationError' - description: Field-level validation errors (for validation failures) - description: RFC 9457 Problem Details error format with HyperFleet extensions + body: + $ref: '#/components/schemas/ForbiddenDetails' + description: The client does not have permission to perform this action. ForceDeleteRequest: type: object required: @@ -1953,6 +2336,19 @@ components: NodePool status computed from all status conditions. This object is computed by the service and CANNOT be modified directly. + NotFoundDetails: + type: object + allOf: + - $ref: '#/components/schemas/ProblemDetails' + example: + type: https://api.hyperfleet.io/errors/resource-not-found + title: Resource Not Found + status: 404 + detail: Cluster with id='019466a0-8f8e-7abc-9def-0123456789ab' not found + instance: /api/hyperfleet/v1/clusters/019466a0-8f8e-7abc-9def-0123456789ab + code: HYPERFLEET-NTF-001 + timestamp: '2024-01-15T10:30:00Z' + trace_id: deadbeef12345678 ObjectReference: type: object properties: @@ -1970,6 +2366,55 @@ components: enum: - asc - desc + ProblemDetails: + type: object + required: + - type + - title + - status + properties: + type: + type: string + format: uri + description: URI reference identifying the problem type + title: + type: string + description: Short human-readable summary of the problem + status: + type: integer + description: HTTP status code + detail: + type: string + description: Human-readable explanation specific to this occurrence + instance: + type: string + format: uri-reference + description: URI reference for this specific occurrence + code: + type: string + description: Machine-readable error code in HYPERFLEET-CAT-NUM format + timestamp: + type: string + format: date-time + description: RFC3339 timestamp of when the error occurred + trace_id: + type: string + description: Distributed trace ID for correlation + errors: + type: array + items: + $ref: '#/components/schemas/ValidationError' + description: Field-level validation errors (for validation failures) + description: RFC 9457 Problem Details for HTTP APIs + example: + type: https://api.hyperfleet.io/errors/internal-error + title: Internal Server Error + status: 500 + detail: Unspecified error + instance: /api/hyperfleet/v1/clusters + code: HYPERFLEET-INT-001 + timestamp: '2024-01-15T10:30:00Z' + trace_id: f0f0f0f0a1a1a1a1 Resource: type: object required: @@ -2213,6 +2658,19 @@ components: description: |- Aggregated status of the resource, populated by the status aggregation pipeline from adapter condition reports. + UnauthorizedDetails: + type: object + allOf: + - $ref: '#/components/schemas/ProblemDetails' + example: + type: https://api.hyperfleet.io/errors/authentication-required + title: Authentication Required + status: 401 + detail: Account authentication could not be verified + instance: /api/hyperfleet/v1/clusters + code: HYPERFLEET-AUT-001 + timestamp: '2024-01-15T10:30:00Z' + trace_id: '1122334455667788' ValidationError: type: object required: diff --git a/shared/models/common/example_errors.tsp b/shared/models/common/example_errors.tsp new file mode 100644 index 0000000..4f5f44d --- /dev/null +++ b/shared/models/common/example_errors.tsp @@ -0,0 +1,65 @@ +const ExampleError400 = #{ + type: "https://api.hyperfleet.io/errors/validation-error", + title: "Validation Failed", + status: 400, + detail: "The cluster name field is required", + instance: "/api/hyperfleet/v1/clusters", + code: "HYPERFLEET-VAL-001", + timestamp: "2024-01-15T10:30:00Z", + trace_id: "aabbccddeeff0011" +}; + +const ExampleError401 = #{ + type: "https://api.hyperfleet.io/errors/authentication-required", + title: "Authentication Required", + status: 401, + detail: "Account authentication could not be verified", + instance: "/api/hyperfleet/v1/clusters", + code: "HYPERFLEET-AUT-001", + timestamp: "2024-01-15T10:30:00Z", + trace_id: "1122334455667788" +}; + +const ExampleError403 = #{ + type: "https://api.hyperfleet.io/errors/permission-denied", + title: "Permission Denied", + status: 403, + detail: "You do not have permission to perform this action", + instance: "/api/hyperfleet/v1/clusters", + code: "HYPERFLEET-AUZ-001", + timestamp: "2024-01-15T10:30:00Z", + trace_id: "99aabbcc11223344" +}; + +const ExampleError404 = #{ + type: "https://api.hyperfleet.io/errors/resource-not-found", + title: "Resource Not Found", + status: 404, + detail: "Cluster with id='019466a0-8f8e-7abc-9def-0123456789ab' not found", + instance: "/api/hyperfleet/v1/clusters/019466a0-8f8e-7abc-9def-0123456789ab", + code: "HYPERFLEET-NTF-001", + timestamp: "2024-01-15T10:30:00Z", + trace_id: "deadbeef12345678" +}; + +const ExampleError409 = #{ + type: "https://api.hyperfleet.io/errors/resource-conflict", + title: "Resource Conflict", + status: 409, + detail: "This Cluster already exists", + instance: "/api/hyperfleet/v1/clusters", + code: "HYPERFLEET-CNF-001", + timestamp: "2024-01-15T10:30:00Z", + trace_id: "cafe0011aabb9988" +}; + +const ExampleError500 = #{ + type: "https://api.hyperfleet.io/errors/internal-error", + title: "Internal Server Error", + status: 500, + detail: "Unspecified error", + instance: "/api/hyperfleet/v1/clusters", + code: "HYPERFLEET-INT-001", + timestamp: "2024-01-15T10:30:00Z", + trace_id: "f0f0f0f0a1a1a1a1" +}; diff --git a/shared/models/common/model.tsp b/shared/models/common/model.tsp index 21603fa..0b2159f 100644 --- a/shared/models/common/model.tsp +++ b/shared/models/common/model.tsp @@ -1,6 +1,8 @@ import "@typespec/http"; import "@typespec/openapi"; import "../statuses/model.tsp"; +import "./example_errors.tsp"; + using Http; using OpenAPI; @@ -56,55 +58,98 @@ model ValidationError { message: string; } -/** - * RFC 9457 Problem Details error format with HyperFleet extensions - */ -@defaultResponse -model Error { - @header contentType: "application/problem+json"; - /** URI reference identifying the problem type */ - @format("uri") - @example("https://api.hyperfleet.io/errors/validation-error") - type: string; - - /** Short human-readable summary of the problem */ - @example("Validation Failed") - title: string; - - /** HTTP status code */ - @example(400) - status: integer; - - /** Human-readable explanation specific to this occurrence */ - @example("The cluster name field is required") - detail?: string; - - /** URI reference for this specific occurrence */ - @format("uri-reference") - @example("/api/hyperfleet/v1/clusters") - instance?: string; - - /** Machine-readable error code in HYPERFLEET-CAT-NUM format */ - @example("HYPERFLEET-VAL-001") - code?: string; - - /** RFC3339 timestamp of when the error occurred */ - @format("date-time") - @example("2024-01-15T10:30:00Z") - timestamp?: string; - - /** Distributed trace ID for correlation */ - @example("abc123def456") - trace_id?: string; - - /** Field-level validation errors (for validation failures) */ - errors?: ValidationError[]; -} - -model ErrorResponse { - @statusCode statusCode: ErrorCode; - @header contentType: "application/problem+json"; - @body error: Error; +namespace HyperFleet { + /** + * RFC 9457 Problem Details for HTTP APIs + */ + @example(ExampleError500) + model ProblemDetails { + /** URI reference identifying the problem type */ + @format("uri") + type: string; + + /** Short human-readable summary of the problem */ + title: string; + + /** HTTP status code */ + status: integer; + + /** Human-readable explanation specific to this occurrence */ + detail?: string; + + /** URI reference for this specific occurrence */ + @format("uri-reference") + instance?: string; + + /** Machine-readable error code in HYPERFLEET-CAT-NUM format */ + code?: string; + + /** RFC3339 timestamp of when the error occurred */ + @format("date-time") + timestamp?: string; + + /** Distributed trace ID for correlation */ + trace_id?: string; + + /** Field-level validation errors (for validation failures) */ + errors?: ValidationError[]; + } + + @example(ExampleError400) + model BadRequestDetails extends ProblemDetails {} + + @example(ExampleError401) + model UnauthorizedDetails extends ProblemDetails {} + + @example(ExampleError403) + model ForbiddenDetails extends ProblemDetails {} + + @example(ExampleError404) + model NotFoundDetails extends ProblemDetails {} + + @example(ExampleError409) + model ConflictDetails extends ProblemDetails {} + + @defaultResponse + model Error { + @header contentType: "application/problem+json"; + @body body: ProblemDetails; + } + + @doc("The server could not understand the request due to invalid syntax.") + model BadRequestResponse { + @statusCode statusCode: 400; + @header contentType: "application/problem+json"; + @body body: BadRequestDetails; + } + + @doc("The request requires valid authentication credentials.") + model UnauthorizedResponse { + @statusCode statusCode: 401; + @header contentType: "application/problem+json"; + @body body: UnauthorizedDetails; + } + + @doc("The client does not have permission to perform this action.") + model ForbiddenResponse { + @statusCode statusCode: 403; + @header contentType: "application/problem+json"; + @body body: ForbiddenDetails; + } + + @doc("The server cannot find the requested resource.") + model NotFoundResponse { + @statusCode statusCode: 404; + @header contentType: "application/problem+json"; + @body body: NotFoundDetails; + } + + @doc("The request conflicts with the current state of the server.") + model ConflictResponse { + @statusCode statusCode: 409; + @header contentType: "application/problem+json"; + @body body: ConflictDetails; + } } model APIResource { @@ -154,9 +199,11 @@ model SearchParams { model QueryParams { ...SearchParams; @query + @minValue(1) page?: int32 = 1; @query + @minValue(1) pageSize?: int32 = 20; @query @@ -244,26 +291,26 @@ model ResourceCondition { @format("date-time") last_updated_time: string; } - const ExampleLastKnownReconciledReason: string = "AllAdaptersReconciled"; - const ExampleLastKnownReconciledMessage: string = "All required adapters report Available=True for the tracked generation"; - const ExampleReconciledReason: string = "ReconciledAll"; - const ExampleReconciledMessage: string = "All required adapters reported Available=True or Finalized=True at the current generation"; - const ExampleAdapter1: string = "adapter1"; - const ExampleAdapter2: string = "adapter2"; - const ExampleAdapter1AppliedReason: string = "Validation job applied"; - const ExampleAdapter1AppliedMessage: string = "Adapter1 validation job applied successfully"; - const ExampleAdapter1HealthReason: string = "All adapter1 operations completed successfully"; - const ExampleAdapter1HealthMessage: string = "All adapter1 runtime operations completed successfully"; - const ExampleAdapter1AvailableReason: string = "This adapter1 is available"; - const ExampleAdapter1AvailableMessage: string = "This adapter1 is available"; - const ExampleAdapter1FinalizedReason: string = "All resources deleted; cleanup confirmed"; - const ExampleAdapter1FinalizedMessage: string = "All resources deleted; cleanup confirmed"; - const ExampleAdapter2AppliedReason: string = "Validation job applied"; - const ExampleAdapter2AppliedMessage: string = "Adapter2 validation job applied successfully"; - const ExampleAdapter2HealthReason: string = "All adapter2 operations completed successfully"; - const ExampleAdapter2HealthMessage: string = "All adapter2 runtime operations completed successfully"; - const ExampleAdapter2AvailableReason: string = "This adapter2 is available"; - const ExampleAdapter2AvailableMessage: string = "This adapter2 is available"; +const ExampleLastKnownReconciledReason: string = "AllAdaptersReconciled"; +const ExampleLastKnownReconciledMessage: string = "All required adapters report Available=True for the tracked generation"; +const ExampleReconciledReason: string = "ReconciledAll"; +const ExampleReconciledMessage: string = "All required adapters reported Available=True or Finalized=True at the current generation"; +const ExampleAdapter1: string = "adapter1"; +const ExampleAdapter2: string = "adapter2"; +const ExampleAdapter1AppliedReason: string = "Validation job applied"; +const ExampleAdapter1AppliedMessage: string = "Adapter1 validation job applied successfully"; +const ExampleAdapter1HealthReason: string = "All adapter1 operations completed successfully"; +const ExampleAdapter1HealthMessage: string = "All adapter1 runtime operations completed successfully"; +const ExampleAdapter1AvailableReason: string = "This adapter1 is available"; +const ExampleAdapter1AvailableMessage: string = "This adapter1 is available"; +const ExampleAdapter1FinalizedReason: string = "All resources deleted; cleanup confirmed"; +const ExampleAdapter1FinalizedMessage: string = "All resources deleted; cleanup confirmed"; +const ExampleAdapter2AppliedReason: string = "Validation job applied"; +const ExampleAdapter2AppliedMessage: string = "Adapter2 validation job applied successfully"; +const ExampleAdapter2HealthReason: string = "All adapter2 operations completed successfully"; +const ExampleAdapter2HealthMessage: string = "All adapter2 runtime operations completed successfully"; +const ExampleAdapter2AvailableReason: string = "This adapter2 is available"; +const ExampleAdapter2AvailableMessage: string = "This adapter2 is available"; /** * Request body for force-delete operations diff --git a/shared/services/clusters.tsp b/shared/services/clusters.tsp index ee73c17..b80a814 100644 --- a/shared/services/clusters.tsp +++ b/shared/services/clusters.tsp @@ -21,7 +21,8 @@ interface Clusters { @doc("Returns a list of all clusters.") getClusters(...QueryParams): Body | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse; @route("/{cluster_id}") @get @@ -33,7 +34,9 @@ interface Clusters { @path cluster_id: string, ): Cluster | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; /** * Create a new cluster resource. @@ -52,7 +55,9 @@ interface Clusters { @statusCode statusCode: 201; @body cluster: Cluster; } | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | ConflictResponse; /** * Patch a specific cluster by ID @@ -67,8 +72,9 @@ interface Clusters { @body body: ClusterPatchRequest, ): Cluster | Error - | NotFoundResponse | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; /** @@ -86,8 +92,9 @@ interface Clusters { ): { @statusCode statusCode: 202; @body cluster: Cluster; - } | NotFoundResponse - | Error - | BadRequestResponse; + } | Error + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; } diff --git a/shared/services/nodepools.tsp b/shared/services/nodepools.tsp index 9b9a536..99cf06a 100644 --- a/shared/services/nodepools.tsp +++ b/shared/services/nodepools.tsp @@ -22,7 +22,8 @@ interface NodePools { @operationId("getNodePools") getNodePools(...QueryParams): Body | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse; /** * Returns the list of all nodepools for a cluster @@ -38,7 +39,8 @@ interface NodePools { ...QueryParams, ): Body | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse; /** * Create a NodePool for a cluster @@ -56,6 +58,8 @@ interface NodePools { (CreatedResponse & NodePoolCreateResponse) | Error | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; /** @@ -72,7 +76,9 @@ interface NodePools { @path nodepool_id: string, ): NodePool | Error - | BadRequestResponse; + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; /** * Marks the nodepool for deletion by setting deleted_time and deleted_by. Does not affect the parent cluster. @@ -91,9 +97,10 @@ interface NodePools { ): { @statusCode statusCode: 202; @body nodepool: NodePool; - } | NotFoundResponse - | Error - | BadRequestResponse; + } | Error + | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse; /** * Patch a specific nodepool within a cluster @@ -110,7 +117,8 @@ interface NodePools { @body body: NodePoolPatchRequest, ): NodePool | Error - | NotFoundResponse | BadRequestResponse + | UnauthorizedResponse + | NotFoundResponse | ConflictResponse; }