Skip to content

HYPERFLEET-545 - feat: add PATCH operations for clusters and nodepools#32

Merged
openshift-merge-bot[bot] merged 1 commit into
openshift-hyperfleet:mainfrom
Mischulee:HYPERFLEET-545
Apr 21, 2026
Merged

HYPERFLEET-545 - feat: add PATCH operations for clusters and nodepools#32
openshift-merge-bot[bot] merged 1 commit into
openshift-hyperfleet:mainfrom
Mischulee:HYPERFLEET-545

Conversation

@Mischulee
Copy link
Copy Markdown
Contributor

@Mischulee Mischulee commented Apr 14, 2026

Summary

Adds PATCH operations for clusters and nested nodepools.

Test Plan

  • npm run build:all passes (both core and GCP)

Summary by CodeRabbit

  • New Features

    • Added PATCH endpoints to support partial updates of clusters and node pools.
    • Added patch request schemas enforcing at-least-one-field and no-extra-fields rules, with example payloads to guide usage.
    • PATCH responses now explicitly include Bad Request and Not Found outcomes.
  • Chores

    • API version bumped to 1.0.7.

@openshift-ci openshift-ci Bot requested review from pnguyen44 and rh-amarin April 14, 2026 08:53
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Walkthrough

Bumps API version to 1.0.7 and adds PATCH endpoints for clusters and nodepools with new request models ClusterPatchRequest and NodePoolPatchRequest (both require ≥1 top-level property and disallow additionalProperties) and attached examples. OpenAPI/Swagger specs updated for core and GCP (including spec update variants for platform-specific patch shapes). Service interfaces add patchClusterById and patchNodePoolById with @patch(implicitOptionality: true) and response unions including BadRequest/NotFound. New example fixtures and alias imports for patch requests were added. Minor formatting/import tweaks applied.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as "API Gateway / HyperFleet API"
    participant Auth as "Auth (BearerAuth)"
    participant Service as "HyperFleet Service"
    participant Store as "DataStore"

    Client->>API: PATCH /api/hyperfleet/v1/clusters/{cluster_id}\nBody: ClusterPatchRequest (Bearer token)
    API->>Auth: Validate token
    Auth-->>API: token OK
    API->>Service: patchClusterById(cluster_id, body)
    Service->>Store: Read & apply partial updates
    Store-->>Service: Updated Cluster
    Service-->>API: 200 (Cluster) or 400/404/Error
    API-->>Client: 200 (Cluster) or Error
Loading
sequenceDiagram
    participant Client
    participant API as "API Gateway / HyperFleet API"
    participant Auth as "Auth (BearerAuth)"
    participant Service as "HyperFleet Service"
    participant Store as "DataStore"

    Client->>API: PATCH /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}\nBody: NodePoolPatchRequest (Bearer token)
    API->>Auth: Validate token
    Auth-->>API: token OK
    API->>Service: patchNodePoolById(cluster_id, nodepool_id, body)
    Service->>Store: Read & apply partial updates to NodePool
    Store-->>Service: Updated NodePool
    Service-->>API: 200 (NodePool) or 400/404/Error
    API-->>Client: 200 (NodePool) or Error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding PATCH operations for clusters and nodepools, which aligns perfectly with all the changes made across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@schemas/gcp/openapi.yaml`:
- Around line 863-867: Update the six PATCH-related schemas to reject empty
objects and unknown keys by adding "additionalProperties: false" and
"minProperties: 1" to each schema: ClusterPatchRequest,
ClusterPlatformSpecUpdate, ClusterSpecUpdate, NodePoolPatchRequest,
NodePoolPlatformSpecUpdate, and NodePoolSpecUpdate; ensure these constraints are
applied inside each schema object so PATCH requestBody validation fails for
empty payloads or typos while keeping requestBody.required: true on the
endpoints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fb207614-e9f4-4dee-b8ac-b8705d2e078c

📥 Commits

Reviewing files that changed from the base of the PR and between 0f0a404 and bbd86ec.

📒 Files selected for processing (9)
  • main.tsp
  • models/clusters/model.tsp
  • models/nodepools/model.tsp
  • schemas/core/openapi.yaml
  • schemas/core/swagger.yaml
  • schemas/gcp/openapi.yaml
  • schemas/gcp/swagger.yaml
  • services/clusters.tsp
  • services/nodepools.tsp

Comment thread schemas/gcp/openapi.yaml
Comment thread models/clusters/model.tsp
Comment on lines +61 to +64
model ClusterPatchRequest {
spec?: ClusterSpec;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that labels should be also possible to update in a PATCH request

Same for nodepools

The JIRA ticket says:

Not in scope: Labels in PATCH — out of scope per epic. Handler/service implementation — already exists.

But that means that the implementation may be already in the code, but is not in the API spec, so IMO it should be added to the API spec.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec may also benefit from having an example,
take a look at the create operation

@example(exampleClusterCreateRequest)
model ClusterCreateRequest  {
    ...ClusterBase;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Labels were already in the model but not applied by the PATCH handlers. I have added the handling and exposed labels in both ClusterPatchRequest and NodePoolPatchRequest in the api spec.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
models-core/nodepool/example_patch.tsp (1)

4-6: Avoid shipping a no-op PATCH example.

models/nodepools/model.tsp (Lines 60-62) uses this value as the public NodePoolPatchRequest example. spec: #{} doesn't demonstrate any mutation and makes a no-op request look like the intended client payload. If core can't show a concrete change here, I'd omit the example and keep the real example in provider-specific specs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models-core/nodepool/example_patch.tsp` around lines 4 - 6, The public
example exampleNodePoolPatchRequest currently sets spec: #{} which is a no-op;
update the NodePoolPatchRequest example (exampleNodePoolPatchRequest) to either
show a concrete, meaningful mutation in spec (e.g., changing a field that is
commonly patched) or remove/omit the example entirely from models-core so
provider-specific specs can supply real examples; ensure references to the
exampleNodePoolPatchRequest and the spec field in the patch request are adjusted
accordingly.
models-core/cluster/example_patch.tsp (1)

4-6: Avoid shipping a no-op PATCH example.

models/clusters/model.tsp (Lines 62-65) binds this constant to the public ClusterPatchRequest, so spec: #{} becomes the documented example for the new PATCH endpoint. That doesn't show an actual update and makes a no-op payload look intentional. If core has no stable field to mutate, I'd omit the core example and let provider-specific specs carry the concrete PATCH examples.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models-core/cluster/example_patch.tsp` around lines 4 - 6, The
exampleClusterPatchRequest constant currently contains an empty no-op payload
(spec: #{}) which gets published as the public ClusterPatchRequest example;
remove this misleading core example by deleting the exampleClusterPatchRequest
constant (or replace it with a comment stating core has no stable patch fields)
so that provider-specific modules supply real PATCH examples instead; locate the
symbol exampleClusterPatchRequest in the diff and remove or neutralize it rather
than shipping an empty spec.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@models-core/cluster/example_patch.tsp`:
- Around line 4-6: The exampleClusterPatchRequest constant currently contains an
empty no-op payload (spec: #{}) which gets published as the public
ClusterPatchRequest example; remove this misleading core example by deleting the
exampleClusterPatchRequest constant (or replace it with a comment stating core
has no stable patch fields) so that provider-specific modules supply real PATCH
examples instead; locate the symbol exampleClusterPatchRequest in the diff and
remove or neutralize it rather than shipping an empty spec.

In `@models-core/nodepool/example_patch.tsp`:
- Around line 4-6: The public example exampleNodePoolPatchRequest currently sets
spec: #{} which is a no-op; update the NodePoolPatchRequest example
(exampleNodePoolPatchRequest) to either show a concrete, meaningful mutation in
spec (e.g., changing a field that is commonly patched) or remove/omit the
example entirely from models-core so provider-specific specs can supply real
examples; ensure references to the exampleNodePoolPatchRequest and the spec
field in the patch request are adjusted accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6590a4c-b023-45c6-9108-502039358a71

📥 Commits

Reviewing files that changed from the base of the PR and between bbd86ec and d317c12.

📒 Files selected for processing (15)
  • aliases-core.tsp
  • aliases-gcp.tsp
  • main.tsp
  • models-core/cluster/example_patch.tsp
  • models-core/nodepool/example_patch.tsp
  • models-gcp/cluster/example_patch.tsp
  • models-gcp/nodepool/example_patch.tsp
  • models/clusters/model.tsp
  • models/nodepools/model.tsp
  • schemas/core/openapi.yaml
  • schemas/core/swagger.yaml
  • schemas/gcp/openapi.yaml
  • schemas/gcp/swagger.yaml
  • services/clusters.tsp
  • services/nodepools.tsp
✅ Files skipped from review due to trivial changes (5)
  • aliases-core.tsp
  • aliases-gcp.tsp
  • models-gcp/cluster/example_patch.tsp
  • main.tsp
  • schemas/gcp/openapi.yaml
🚧 Files skipped from review as they are similar to previous changes (6)
  • models/nodepools/model.tsp
  • models/clusters/model.tsp
  • services/nodepools.tsp
  • services/clusters.tsp
  • schemas/gcp/swagger.yaml
  • schemas/core/openapi.yaml

Comment thread services/clusters.tsp
} | Error
| BadRequestResponse;

@route("/{cluster_id}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

nit — non-blocking suggestion

Category: Inconsistency

The nodepool PATCH has a JSDoc comment that generates a description field in OpenAPI, but the cluster PATCH doesn't. This causes an inconsistency in the generated specs where patchNodePoolById has a description but patchClusterById does not.

Consider adding a JSDoc comment before @route:

  /**
   * Patch cluster spec fields by cluster ID
   */
  @route("/{cluster_id}")
  @patch(#{implicitOptionality: true})
  @summary("Patch cluster by ID")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description added.

Comment thread aliases-gcp.tsp Outdated
import "./models-gcp/nodepool/example_nodepool.tsp";
import "./models-gcp/nodepool/example_post.tsp"; No newline at end of file
import "./models-gcp/nodepool/example_post.tsp";
import "./models-gcp/nodepool/example_patch.tsp"; No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

nit — non-blocking suggestion

Category: Pattern

Missing trailing newline at end of file. Git shows \ No newline at end of file for this line. Adding a newline after the last import keeps the file POSIX-compliant and avoids noisy diffs if another import is appended later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line added.

Comment thread models/clusters/model.tsp
...ClusterBase;
}

@extension("minProperties", 1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Blocking

Category: Inconsistency

The additionalProperties: false fix discussed in the CodeRabbit thread doesn't appear to have been applied. Neither the TypeSpec models nor the generated schemas contain it.

Current state (models/clusters/model.tsp):

@extension("minProperties", 1)
@example(exampleClusterPatchRequest)
model ClusterPatchRequest {
  spec?: ClusterSpec;
  labels?: Record<string>;
}

Suggested fix — add the decorator to both ClusterPatchRequest and NodePoolPatchRequest:

@extension("minProperties", 1)
@extension("additionalProperties", false)
@example(exampleClusterPatchRequest)
model ClusterPatchRequest {
  spec?: ClusterSpec;
  labels?: Record<string>;
}

Then regenerate the schemas (npm run build:all) so the YAML files pick up the change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread services/clusters.tsp
): Cluster
| Error
| NotFoundResponse
| BadRequestResponse;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

nit — non-blocking suggestion

Category: Standards

The BadRequestResponse used here generates a 400 response without a body schema. Per the HyperFleet error-model standard (RFC 9457), error responses should include a Problem Details body (application/problem+json).

This is a pre-existing pattern across all endpoints, so I wouldn't fix it just for PATCH — but it might be worth a follow-up ticket to define a custom BadRequestResponse model with a Problem Details body and apply it consistently across the API.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
schemas/gcp/openapi.yaml (1)

1088-1121: ⚠️ Potential issue | 🟠 Major

GCP PATCH models still admit empty sub-patches

The top-level minProperties: 1 is not enough here. {"labels": {}}, {"spec": {}}, and {"spec": {"platform": {}}} still validate because the label maps and the *Update objects all allow empty objects, and the nested *Update schemas still accept unknown keys.

Suggested contract tightening
     ClusterPatchRequest:
       type: object
       properties:
         spec:
           $ref: '#/components/schemas/ClusterSpecUpdate'
         labels:
           type: object
+          minProperties: 1
           additionalProperties:
             type: string
@@
     ClusterPlatformSpecUpdate:
       type: object
+      additionalProperties: false
+      minProperties: 1
       properties:
         type:
           type: string
@@
     ClusterSpecUpdate:
       type: object
+      additionalProperties: false
+      minProperties: 1
       properties:
         infraID:
           type: string
@@
     NodePoolPatchRequest:
       type: object
       properties:
         spec:
           $ref: '#/components/schemas/NodePoolSpecUpdate'
         labels:
           type: object
+          minProperties: 1
           additionalProperties:
             type: string
@@
     NodePoolPlatformSpecUpdate:
       type: object
+      additionalProperties: false
+      minProperties: 1
       properties:
         type:
           type: string
@@
     NodePoolSpecUpdate:
       type: object
+      additionalProperties: false
+      minProperties: 1
       properties:
         platform:
           $ref: '#/components/schemas/NodePoolPlatformSpecUpdate'

Also applies to: 1147-1183, 1557-1590, 1635-1665

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@schemas/gcp/openapi.yaml` around lines 1088 - 1121, The ClusterPatchRequest
and other PATCH schemas (e.g., the ClusterSpecUpdate and nested platform/update
schemas referenced by spec and labels) currently allow empty sub-patches; fix by
tightening the schemas: add minProperties: 1 to the labels object, and in each
"*Update" schema referenced (ClusterSpecUpdate and its nested platform/ gcp/
networking/etc. update types) set additionalProperties: false and minProperties:
1 so empty objects are rejected; ensure nested platform (e.g., platform/gcp)
update schemas also require at least one field and disallow unknown keys. Apply
the same changes to the other patch model blocks noted (lines referenced in the
review: the other Cluster*PatchRequest equivalents).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@schemas/core/openapi.yaml`:
- Around line 1126-1140: ClusterPatchRequest currently allows no-op patches
because minProperties: 1 only forces a top-level key and accepts {"spec": {}} or
{"labels": {}}; update ClusterPatchRequest to require at least one non-empty
field by replacing the loose properties rule with an anyOf/oneOf that enforces
non-empty nested objects (e.g., a branch where "spec" is present and its schema
requires minProperties: 1 or references ClusterSpec-with-minProperties, and
another branch where "labels" is present and has minProperties: 1), and apply
the same pattern to the other patch schemas mentioned (lines ~1483-1497) so a
PATCH must change at least one actual field rather than an empty object.

---

Duplicate comments:
In `@schemas/gcp/openapi.yaml`:
- Around line 1088-1121: The ClusterPatchRequest and other PATCH schemas (e.g.,
the ClusterSpecUpdate and nested platform/update schemas referenced by spec and
labels) currently allow empty sub-patches; fix by tightening the schemas: add
minProperties: 1 to the labels object, and in each "*Update" schema referenced
(ClusterSpecUpdate and its nested platform/ gcp/ networking/etc. update types)
set additionalProperties: false and minProperties: 1 so empty objects are
rejected; ensure nested platform (e.g., platform/gcp) update schemas also
require at least one field and disallow unknown keys. Apply the same changes to
the other patch model blocks noted (lines referenced in the review: the other
Cluster*PatchRequest equivalents).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cf0d1374-75b2-4d94-95b0-ac7ab32b54e8

📥 Commits

Reviewing files that changed from the base of the PR and between f54f2b1 and 5c00354.

📒 Files selected for processing (15)
  • aliases-core.tsp
  • aliases-gcp.tsp
  • main.tsp
  • models-core/cluster/example_patch.tsp
  • models-core/nodepool/example_patch.tsp
  • models-gcp/cluster/example_patch.tsp
  • models-gcp/nodepool/example_patch.tsp
  • models/clusters/model.tsp
  • models/nodepools/model.tsp
  • schemas/core/openapi.yaml
  • schemas/core/swagger.yaml
  • schemas/gcp/openapi.yaml
  • schemas/gcp/swagger.yaml
  • services/clusters.tsp
  • services/nodepools.tsp
✅ Files skipped from review due to trivial changes (4)
  • aliases-gcp.tsp
  • models-core/nodepool/example_patch.tsp
  • main.tsp
  • aliases-core.tsp
🚧 Files skipped from review as they are similar to previous changes (7)
  • models/nodepools/model.tsp
  • models-gcp/cluster/example_patch.tsp
  • models-gcp/nodepool/example_patch.tsp
  • services/clusters.tsp
  • services/nodepools.tsp
  • models-core/cluster/example_patch.tsp
  • models/clusters/model.tsp

Comment thread schemas/core/openapi.yaml
@rh-amarin
Copy link
Copy Markdown
Collaborator

/lgtm

@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Apr 21, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: rh-amarin

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-merge-bot openshift-merge-bot Bot merged commit fe59c7a into openshift-hyperfleet:main Apr 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants