Skip to content

feat: Add support for namespacedcloudprofiles#2691

Open
klocke-io wants to merge 4 commits into
masterfrom
feature/namespaced-cloud-profile-support
Open

feat: Add support for namespacedcloudprofiles#2691
klocke-io wants to merge 4 commits into
masterfrom
feature/namespaced-cloud-profile-support

Conversation

@klocke-io
Copy link
Copy Markdown
Member

@klocke-io klocke-io commented Nov 18, 2025

What this PR does / why we need it:
Add support for namespacedcloudprofiles

Which issue(s) this PR fixes:
Fixes # #1971

Special notes for your reviewer:
NSCP in create cluster dialog

image

No more warnings because the cloudprofile is not understood
image
image
image
image

Release note:

Add namespacedcloudprofiles support

Summary by CodeRabbit

  • New Features

    • Namespaced CloudProfile support across backend and frontend, including API endpoints and RBAC permissions.
    • Optional diff view for namespaced profiles to compare against parent profiles and rehydrate specs when needed.
    • Cloud profile selector and store UI updated to show regular and namespaced profiles uniformly; namespace-aware loading.
  • Tests

    • Expanded test coverage for namespaced profiles, diffing, rehydration and authorization scenarios.

@ghost ghost added needs/review Needs review size/L Denotes a PR that changes 100-499 lines, ignoring generated files. needs/second-opinion Needs second review by someone else labels Nov 18, 2025
@klocke-io klocke-io force-pushed the feature/namespaced-cloud-profile-support branch 3 times, most recently from a561dff to 674686f Compare November 19, 2025 08:41
@ghost
Copy link
Copy Markdown

ghost commented Nov 21, 2025

@holgerkoser, @grolu You have pull request review open invite, please check

@grolu grolu added area/ipcei IPCEI (Important Project of Common European Interest) and removed area/ipcei IPCEI (Important Project of Common European Interest) labels Dec 1, 2025
Comment thread backend/lib/routes/index.js Outdated
'/projects': projectsRoute,
'/namespaces/:namespace/shoots': shootsRoute,
'/namespaces/:namespace/tickets': ticketsRoute,
'/namespaces/:namespace/cloudprofiles': namespacedCloudProfilesRoute,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's align closer to the gardener API (/apis/core.gardener.cloud/v1beta1/namespaces/garden-core/namespacedcloudprofiles)

Suggested change
'/namespaces/:namespace/cloudprofiles': namespacedCloudProfilesRoute,
'/namespaces/:namespace/namespacedcloudprofiles': namespacedCloudProfilesRoute,

Comment thread backend/lib/routes/namespacedCloudProfiles.js
@gardener-ci-robot
Copy link
Copy Markdown
Contributor

The Gardener project currently lacks enough active contributors to adequately respond to all PRs.
This bot triages PRs according to the following rules:

  • After 15d of inactivity, lifecycle/stale is applied
  • After 15d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 7d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Mark this PR as fresh with /remove-lifecycle stale
  • Mark this PR as rotten with /lifecycle rotten
  • Close this PR with /close

/lifecycle stale

@ghost ghost added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Dec 23, 2025
@gardener-ci-robot
Copy link
Copy Markdown
Contributor

The Gardener project currently lacks enough active contributors to adequately respond to all PRs.
This bot triages PRs according to the following rules:

  • After 15d of inactivity, lifecycle/stale is applied
  • After 15d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 7d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Mark this PR as fresh with /remove-lifecycle rotten
  • Close this PR with /close

/lifecycle rotten

@ghost ghost added lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. and removed lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. labels Jan 7, 2026
@grolu grolu added lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. lifecycle/frozen Indicates that an issue or PR should not be auto-closed due to staleness. and removed lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. labels Jan 14, 2026
@klocke-io
Copy link
Copy Markdown
Member Author

/remove-lifecycle rotten

@klocke-io klocke-io removed the lifecycle/frozen Indicates that an issue or PR should not be auto-closed due to staleness. label Jan 26, 2026
@klocke-io klocke-io self-assigned this Jan 26, 2026
@klocke-io klocke-io marked this pull request as draft January 26, 2026 14:05
@gardener-prow gardener-prow Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jan 26, 2026
@klocke-io klocke-io force-pushed the feature/namespaced-cloud-profile-support branch from 674686f to a3a62e7 Compare January 27, 2026 11:40
@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented Jan 27, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from klocke-io. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@gardener-prow gardener-prow Bot added cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Feb 3, 2026
@klocke-io klocke-io force-pushed the feature/namespaced-cloud-profile-support branch from 23fa0b5 to 6d52a64 Compare March 3, 2026 13:08
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Adds support for NamespacedCloudProfile resources end-to-end: API, service, cache, informer, RBAC, fixtures, frontend store/composables/components, diff-based rehydration using jsondiffpatch, test coverage, and related tooling/config updates.

NamespacedCloudProfile feature

Layer / File(s) Summary
Data Shape / Resource Definition
packages/kube-client/lib/resources/GardenerCore.js, charts/gardener-dashboard/.../clusterrole.yaml
Introduces NamespacedCloudProfile resource metadata and adds RBAC rule granting list/watch for namespacedcloudprofiles.
Cache / Informers
backend/lib/cache/index.js, backend/lib/hooks.js
Cache gains getNamespacedCloudProfiles and getCloudProfile with deep-cloning; lifecycle hooks register an informer for namespacedcloudprofiles.
Fixtures
backend/__fixtures__/namespacedCloudprofiles.cjs, backend/__fixtures__/index.cjs, backend/__fixtures__/cloudprofiles.cjs
New namespacedCloudprofiles fixture (list/get/create/reset) and register it; cloudprofile fixtures now include metadata.resourceVersion.
Core Service Logic
backend/lib/services/namespacedCloudProfiles.js, backend/lib/services/authorization.js, backend/lib/services/cloudprofiles.js, backend/lib/services/index.js
Adds listForNamespace({ user, namespace, diff }) with authorization check (canListNamespacedCloudProfiles), computes JSON diffs against parent CloudProfile, returns diff-transformed or simplified profiles; cloudprofiles read/list now use getCloudProfile/simplifyCloudProfile.
Routes / API
backend/lib/routes/namespacedCloudProfiles.js, backend/lib/routes/index.js, backend/lib/routes/cloudprofiles.js
New GET /api/namespaces/:namespace/namespacedcloudprofiles (supports ?diff=true) and GET /api/cloudprofiles/:name route wired into router index.
Utilities / Hashing / Diff helpers
backend/lib/utils/index.js, frontend/src/utils/crypto.js, frontend/src/utils/index.js
Adds stripProviderConfig, simplifyCloudProfile, computeSpecHash (backend) and frontend getCloudProfileSpec, sha256, computeSpecHash using canonicalize for stable hashing.
Frontend Store & Rehydration
frontend/src/store/cloudProfile/index.js, frontend/src/store/seed.js
Pinia store: namespaced state (namespacedList, namespacedListsByNamespace), fetch/set APIs, rehydrateNamespacedProfiles applying jsondiffpatch patches, hash validation, parent-refetch logic, merged allCloudProfiles, and updated cloudProfileByRef (namespace-aware). Seed matcher updated to use getCloudProfileSpec.
Frontend Composables & Component changes
frontend/src/composables/*, frontend/src/components/GSelectCloudProfile.vue, frontend/src/composables/useApi/api.js, frontend/src/composables/useShootHelper.js
Multiple composables now call getCloudProfileSpec; GSelectCloudProfile unified to single cloudProfiles prop, shows namespaced chip, removed namespacedCloudProfiles prop; API adds getCloudProfile and getNamespacedCloudProfiles.
Tests & Mocks
backend/__tests__/*, backend/__mocks__/jsondiffpatch.cjs, frontend/__tests__/*, backend/jest.config.cjs, backend/jest.setup.cjs, backend/vitest.config.js
Adds acceptance and unit tests for namespaced APIs, service diffing and rehydration, frontend store behaviors; introduces a minimal CommonJS mock for jsondiffpatch (Jest mapping) and Vitest config; adjusts Jest config/test matching.
Tooling / Dependency map
.pnp.cjs, backend/package.json, frontend/package.json
Adds jsondiffpatch, canonicalize, Vitest and coverage devDeps; updates Yarn PnP map with jsondiffpatch and Vitest virtual entries.
sequenceDiagram
    participant Client
    participant FrontendStore
    participant Server
    participant Cache
    participant Auth
    participant jsondiffpatch

    Client->>FrontendStore: request namespaced cloudprofiles (namespace, diff=true)
    FrontendStore->>Server: GET /api/namespaces/:ns/namespacedcloudprofiles?diff=true
    Server->>Auth: check canListNamespacedCloudProfiles(user, namespace)
    Auth-->>Server: allowed/denied
    Server->>Cache: getNamespacedCloudProfiles(namespace)
    Cache-->>Server: namespaced profiles
    Server->>Cache: getCloudProfile(parentName) [for each profile with diff]
    Cache-->>Server: parent profile (or null)
    Server->>jsondiffpatch: diff(parentSpec, childSpec) / patch(...) as needed
    jsondiffpatch-->>Server: diff / patched spec
    Server-->>Client: JSON response (diff-transformed or simplified profiles)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

kind/enhancement

Suggested reviewers

  • holgerkoser
  • grolu

Poem

🐰 I patch the specs by moonlit code,

Namespaced profiles find their road,
Diffs stitched neat with hashed delight,
From backend burrows to frontend light,
Hop—clouds align and tests all bode.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main feature being added: support for namespacedcloudprofiles, which aligns with the primary change across the changeset.
Description check ✅ Passed The PR description includes key sections (what it does, issue reference, special notes, release note) matching the template structure, though some template fields like /area and /kind categorization are incomplete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/namespaced-cloud-profile-support
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/namespaced-cloud-profile-support

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@klocke-io klocke-io marked this pull request as ready for review March 3, 2026 13:09
@gardener-prow gardener-prow Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 3, 2026
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: 4

🧹 Nitpick comments (7)
frontend/src/composables/useShootMessages.js (1)

49-49: Prefer a more specific JSDoc shape than object for better DX.

Ref<object> works, but a minimal structural type would preserve IntelliSense while still covering both profile kinds.

♻️ Optional JSDoc refinement
- * `@param` {Ref<object>} cloudProfile - A Vue ref containing a CloudProfile or NamespacedCloudProfile object
+ * `@param` {Ref<{ kind?: string, spec?: object }>} cloudProfile - A Vue ref containing a CloudProfile-like object
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/composables/useShootMessages.js` at line 49, The JSDoc for the
cloudProfile parameter is too generic (Ref<object>) which hurts IntelliSense;
update the JSDoc on the cloudProfile param in useShootMessages (the cloudProfile
param of the function exported from
frontend/src/composables/useShootMessages.js) to a minimal structural type that
covers both CloudProfile and NamespacedCloudProfile (for example a Ref with
metadata and optional namespace fields or an explicit union like
Ref<CloudProfile|NamespacedCloudProfile>) so editors can infer properties like
metadata.name/namespace while still accepting both kinds.
backend/__tests__/acceptance/api.namespacedCloudprofiles.spec.cjs (1)

33-37: Add an explicit 200 assertion in the success path.

This test already validates content type and body; adding .expect(200) makes the contract stricter and clearer.

✅ Suggested tweak
       const res = await agent
         .get(`/api/namespaces/${namespace}/namespacedcloudprofiles`)
         .set('cookie', await user.cookie)
+        .expect(200)
         .expect('content-type', /json/)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/__tests__/acceptance/api.namespacedCloudprofiles.spec.cjs` around
lines 33 - 37, The test's request chain lacks an explicit HTTP status assertion,
so update the agent GET call to assert a 200 OK response by adding .expect(200)
in the request chain (e.g., on the
agent.get(`/api/namespaces/${namespace}/namespacedcloudprofiles`) call that
currently sets 'cookie' and expects 'content-type' and inspects res.body);
ensure .expect(200) is placed before or alongside the existing
.expect('content-type', /json/) to make the success contract explicit.
frontend/src/composables/useApi/api.js (1)

239-242: Consider making diff configurable in the API helper.

Hardcoding diff=true couples all callers to diff computation. Exposing a parameter (default as needed) keeps this endpoint reusable and avoids unnecessary backend work.

♻️ Proposed refactor
-export function getNamespacedCloudProfiles ({ namespace }) {
+export function getNamespacedCloudProfiles ({ namespace, diff = true }) {
   namespace = encodeURIComponent(namespace)
-  return getResource(`/api/namespaces/${namespace}/namespacedcloudprofiles?diff=true`)
+  const search = new URLSearchParams({ diff: String(diff) }).toString()
+  return getResource(`/api/namespaces/${namespace}/namespacedcloudprofiles?${search}`)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/composables/useApi/api.js` around lines 239 - 242, Update
getNamespacedCloudProfiles to accept a diff option instead of hardcoding
diff=true: modify the function signature (getNamespacedCloudProfiles) to take an
options object (e.g., { namespace, diff = true }) or an extra parameter,
url-encode namespace as before, build the query string to include diff only when
provided (using the diff value as default true if desired), and call getResource
with `/api/namespaces/${namespace}/namespacedcloudprofiles?diff=${diff}` (or
conditionally append the diff param) so callers can override or omit diff to
avoid forcing diff computation.
backend/lib/utils/index.js (1)

150-185: Consider making simplifyCloudProfile non-mutating.

At Line 168 and Line 182, the function mutates the passed object. Cloning internally would reduce side-effect risk when future callers pass shared references.

♻️ Suggested refactor
 function simplifyCloudProfile (profile) {
+  profile = _.cloneDeep(profile)
   const kind = _.get(profile, ['kind'])

   let entrypath = ['spec']
   if (kind === 'NamespacedCloudProfile') {
     entrypath = ['status', 'cloudProfileSpec']
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/utils/index.js` around lines 150 - 185, simplifyCloudProfile
currently mutates its input by calling simplifyObjectMetadata(profile) and
_.set(profile, [...entrypath, 'providerConfig'], strippedProviderConfig); to
make it non-mutating, create a deep clone of the incoming profile at the start
(e.g., cloneProfile = _.cloneDeep(profile)) and operate on that clone throughout
(pass cloneProfile into simplifyObjectMetadata and set the providerConfig on
cloneProfile instead of profile), and when calling stripProviderConfig ensure
you also clone providerConfig before passing it if stripProviderConfig mutates
its input; return the cloned/modified object (cloneProfile) and leave the
original profile unchanged, referencing the simplifyCloudProfile function,
profile variable, providerConfig variable, simplifyObjectMetadata and
stripProviderConfig helpers, and the _.set usage to locate the changes.
frontend/src/composables/useShootHelper.js (1)

102-107: Avoid namespace shadowing in defaultCloudProfileRef.

The local namespace at Line 102 shadows the outer namespace computed ref, which makes this block harder to read.

🧹 Small readability tweak
-    const namespace = get(defaultCloudProfile, ['metadata', 'namespace'])
+    const profileNamespace = get(defaultCloudProfile, ['metadata', 'namespace'])
     return {
       name,
-      kind: namespace ? 'NamespacedCloudProfile' : 'CloudProfile',
-      ...(namespace && { namespace }),
+      kind: profileNamespace ? 'NamespacedCloudProfile' : 'CloudProfile',
+      ...(profileNamespace && { namespace: profileNamespace }),
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/composables/useShootHelper.js` around lines 102 - 107, The local
variable named "namespace" inside the object returned from defaultCloudProfile
(const namespace = get(defaultCloudProfile, ['metadata', 'namespace'])) shadows
the outer computed ref named "namespace" (defaultCloudProfileRef), hurting
readability; rename the local to something like "profileNamespace" (or
"cpNamespace") and update its usage in the returned object (kind:
profileNamespace ? 'NamespacedCloudProfile' : 'CloudProfile',
...(profileNamespace && { namespace: profileNamespace })) so the outer computed
ref remains unshadowed and the code is clearer.
backend/__tests__/services.namespacedCloudProfiles.spec.js (1)

227-230: Prefer name-based assertions over index-based assertions in these tests.

Using profile identity (e.g., metadata.name) instead of fixed positions will make tests less brittle to ordering changes.

Also applies to: 269-272

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

In `@backend/__tests__/services.namespacedCloudProfiles.spec.js` around lines 227
- 230, Replace brittle index-based assertions on result[0]/result[1] with
name-based assertions that check the set of profile names; for example, map
result to an array of metadata.name and use something like
expect(names).toEqual(expect.arrayContaining(['custom-cloudprofile-1','custom-cloudprofile-2']))
(or toContain for each name). Apply the same change to the other test block that
asserts result[0]/result[1] (the second occurrence referenced) so both tests
assert on metadata.name values rather than fixed positions.
backend/lib/services/namespacedCloudProfiles.js (1)

24-39: Consider memoizing parent-spec simplification in diff mode.

When multiple NamespacedCloudProfiles share a parent, getCloudProfile + simplifyCloudProfile are recomputed for each item. Caching by parent name in the request scope would cut repeated work.

Also applies to: 70-72

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

In `@backend/lib/services/namespacedCloudProfiles.js` around lines 24 - 39,
Multiple NamespacedCloudProfiles recompute getCloudProfile +
simplifyCloudProfile for the same parent; add a request-scoped cache (e.g., a
Map keyed by parentName) inside computeDiff to avoid repeated work: before
calling getCloudProfile/simplifyCloudProfile check the cache for parentName, if
present reuse the cached simplified parent or parentSpec, otherwise call
getCloudProfile and simplifyCloudProfile, store the simplified result in the
cache, then proceed to compute jsondiffpatch.diff; apply the same caching
pattern to the other identical call site around lines 70-72 so both places reuse
the request-scoped memoized simplified parent by parentName.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/__tests__/composables/useCloudProfile/useMachineTypes.spec.js`:
- Line 134: The test creates the computed from useFilteredMachineTypes(region,
architecture) but never evaluates it; change the test to capture the returned
computed (e.g., const filtered = useFilteredMachineTypes(region, architecture))
and then read or assign filtered.value so the computed callback runs and the
defaulting/mutation-sensitive logic executes; ensure any assertions use
filtered.value afterward to validate results.

In `@frontend/src/components/GSelectCloudProfile.vue`:
- Around line 38-49: The custom selection slot currently renders item.raw.title
directly which yields a blank label when the selected value isn't in
selectItems; update the selection template (the template `#selection`="{ item }"
slot) to use a safe fallback chain (e.g. item.raw.title || item.title ||
item.text || item.value) so a meaningful label is shown whenever item.raw.title
is empty or undefined and preserve the existing v-chip logic for
item.raw.namespaced.

In `@frontend/src/router/guards.js`:
- Line 73: The variable assigned to namespace (currently using
to.params.namespace ?? to.query.namespace) must be normalized to a single string
because to.query.namespace can be a string[]; update the assignment in guards.js
so that if to.query.namespace is an array you extract a single string (e.g.,
first element) or join/flatten it, and then fallback to to.params.namespace;
ensure the final namespace is a string (use Array.isArray on to.query.namespace
and coerce/trim as needed) before any validation or API calls that use
namespace.

In `@frontend/src/store/cloudProfile/index.js`:
- Around line 69-75: fetchNamespacedCloudProfilesForNamespaces currently only
adds missing namespaces but never removes stale entries, leaving
namespacedList/allCloudProfiles polluted; update this function to prune cache
entries for namespaces that are no longer in scope: compute the canonical set of
requested namespaces (uniq/filter(Boolean)), call the existing
fetchNamespacedCloudProfiles for any missing ones as before, and then remove any
keys from namespacedList and any entries in allCloudProfiles that belong to
namespaces not in that canonical set (use the store's mutation or helper that
manages namespacedList and allCloudProfiles so state stays consistent). Ensure
you reference and update namespacedList and allCloudProfiles within
fetchNamespacedCloudProfilesForNamespaces so removed namespaces are cleared
after the fetch step.

---

Nitpick comments:
In `@backend/__tests__/acceptance/api.namespacedCloudprofiles.spec.cjs`:
- Around line 33-37: The test's request chain lacks an explicit HTTP status
assertion, so update the agent GET call to assert a 200 OK response by adding
.expect(200) in the request chain (e.g., on the
agent.get(`/api/namespaces/${namespace}/namespacedcloudprofiles`) call that
currently sets 'cookie' and expects 'content-type' and inspects res.body);
ensure .expect(200) is placed before or alongside the existing
.expect('content-type', /json/) to make the success contract explicit.

In `@backend/__tests__/services.namespacedCloudProfiles.spec.js`:
- Around line 227-230: Replace brittle index-based assertions on
result[0]/result[1] with name-based assertions that check the set of profile
names; for example, map result to an array of metadata.name and use something
like
expect(names).toEqual(expect.arrayContaining(['custom-cloudprofile-1','custom-cloudprofile-2']))
(or toContain for each name). Apply the same change to the other test block that
asserts result[0]/result[1] (the second occurrence referenced) so both tests
assert on metadata.name values rather than fixed positions.

In `@backend/lib/services/namespacedCloudProfiles.js`:
- Around line 24-39: Multiple NamespacedCloudProfiles recompute getCloudProfile
+ simplifyCloudProfile for the same parent; add a request-scoped cache (e.g., a
Map keyed by parentName) inside computeDiff to avoid repeated work: before
calling getCloudProfile/simplifyCloudProfile check the cache for parentName, if
present reuse the cached simplified parent or parentSpec, otherwise call
getCloudProfile and simplifyCloudProfile, store the simplified result in the
cache, then proceed to compute jsondiffpatch.diff; apply the same caching
pattern to the other identical call site around lines 70-72 so both places reuse
the request-scoped memoized simplified parent by parentName.

In `@backend/lib/utils/index.js`:
- Around line 150-185: simplifyCloudProfile currently mutates its input by
calling simplifyObjectMetadata(profile) and _.set(profile, [...entrypath,
'providerConfig'], strippedProviderConfig); to make it non-mutating, create a
deep clone of the incoming profile at the start (e.g., cloneProfile =
_.cloneDeep(profile)) and operate on that clone throughout (pass cloneProfile
into simplifyObjectMetadata and set the providerConfig on cloneProfile instead
of profile), and when calling stripProviderConfig ensure you also clone
providerConfig before passing it if stripProviderConfig mutates its input;
return the cloned/modified object (cloneProfile) and leave the original profile
unchanged, referencing the simplifyCloudProfile function, profile variable,
providerConfig variable, simplifyObjectMetadata and stripProviderConfig helpers,
and the _.set usage to locate the changes.

In `@frontend/src/composables/useApi/api.js`:
- Around line 239-242: Update getNamespacedCloudProfiles to accept a diff option
instead of hardcoding diff=true: modify the function signature
(getNamespacedCloudProfiles) to take an options object (e.g., { namespace, diff
= true }) or an extra parameter, url-encode namespace as before, build the query
string to include diff only when provided (using the diff value as default true
if desired), and call getResource with
`/api/namespaces/${namespace}/namespacedcloudprofiles?diff=${diff}` (or
conditionally append the diff param) so callers can override or omit diff to
avoid forcing diff computation.

In `@frontend/src/composables/useShootHelper.js`:
- Around line 102-107: The local variable named "namespace" inside the object
returned from defaultCloudProfile (const namespace = get(defaultCloudProfile,
['metadata', 'namespace'])) shadows the outer computed ref named "namespace"
(defaultCloudProfileRef), hurting readability; rename the local to something
like "profileNamespace" (or "cpNamespace") and update its usage in the returned
object (kind: profileNamespace ? 'NamespacedCloudProfile' : 'CloudProfile',
...(profileNamespace && { namespace: profileNamespace })) so the outer computed
ref remains unshadowed and the code is clearer.

In `@frontend/src/composables/useShootMessages.js`:
- Line 49: The JSDoc for the cloudProfile parameter is too generic (Ref<object>)
which hurts IntelliSense; update the JSDoc on the cloudProfile param in
useShootMessages (the cloudProfile param of the function exported from
frontend/src/composables/useShootMessages.js) to a minimal structural type that
covers both CloudProfile and NamespacedCloudProfile (for example a Ref with
metadata and optional namespace fields or an explicit union like
Ref<CloudProfile|NamespacedCloudProfile>) so editors can infer properties like
metadata.name/namespace while still accepting both kinds.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab65b6d and 6d52a64.

⛔ Files ignored due to path filters (5)
  • .yarn/cache/@dmsnell-diff-match-patch-npm-1.1.0-021308d314-8547bf4a62.zip is excluded by !**/.yarn/**, !**/*.zip
  • .yarn/cache/jsondiffpatch-npm-0.7.3-5579555f56-fae49073e5.zip is excluded by !**/.yarn/**, !**/*.zip
  • backend/__tests__/acceptance/__snapshots__/api.namespacedCloudprofiles.spec.cjs.snap is excluded by !**/*.snap
  • charts/__tests__/gardener-dashboard/application/dashboard/__snapshots__/clusterrole.spec.js.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (44)
  • .pnp.cjs
  • backend/__fixtures__/index.cjs
  • backend/__fixtures__/namespacedcloudprofiles.cjs
  • backend/__mocks__/jsondiffpatch.cjs
  • backend/__tests__/acceptance/api.cloudprofiles.spec.cjs
  • backend/__tests__/acceptance/api.namespacedCloudprofiles.spec.cjs
  • backend/__tests__/cache.spec.cjs
  • backend/__tests__/services.namespacedCloudProfiles.spec.js
  • backend/jest.config.cjs
  • backend/jest.setup.cjs
  • backend/lib/cache/index.js
  • backend/lib/hooks.js
  • backend/lib/routes/index.js
  • backend/lib/routes/namespacedCloudProfiles.js
  • backend/lib/services/authorization.js
  • backend/lib/services/cloudprofiles.js
  • backend/lib/services/index.js
  • backend/lib/services/namespacedCloudProfiles.js
  • backend/lib/utils/index.js
  • backend/package.json
  • backend/vitest.config.js
  • charts/gardener-dashboard/charts/application/templates/dashboard/clusterrole.yaml
  • frontend/__tests__/composables/useCloudProfile/useMachineTypes.spec.js
  • frontend/__tests__/stores/cloudProfile.spec.js
  • frontend/__tests__/utils/index.spec.js
  • frontend/package.json
  • frontend/src/components/GSelectCloudProfile.vue
  • frontend/src/composables/helper.js
  • frontend/src/composables/useApi/api.js
  • frontend/src/composables/useCloudProfile/useDefaultNodesCIDR.js
  • frontend/src/composables/useCloudProfile/useKubernetesVersions.js
  • frontend/src/composables/useCloudProfile/useMachineImages.js
  • frontend/src/composables/useCloudProfile/useMachineTypes.js
  • frontend/src/composables/useCloudProfile/useMetalConstraints.js
  • frontend/src/composables/useCloudProfile/useOpenStackConstraints.js
  • frontend/src/composables/useCloudProfile/useRegions.js
  • frontend/src/composables/useCloudProfile/useVolumeTypes.js
  • frontend/src/composables/useShootHelper.js
  • frontend/src/composables/useShootMessages.js
  • frontend/src/router/guards.js
  • frontend/src/store/cloudProfile/index.js
  • frontend/src/store/seed.js
  • frontend/src/utils/index.js
  • packages/kube-client/lib/resources/GardenerCore.js


const region = ref('region2')
const architecture = ref(undefined)
useFilteredMachineTypes(region, architecture)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the composable returns a lazy computed and the test doesn't dereference it.
rg -n -C2 "function useFilteredMachineTypes|return computed" frontend/src/composables/useCloudProfile/useMachineTypes.js

Repository: gardener/dashboard

Length of output: 661


🏁 Script executed:

#!/bin/bash
rg -n -C4 "should not mutate cloud profile machine types when defaulting architecture|useFilteredMachineTypes\(|\\.value" frontend/__tests__/composables/useCloudProfile/useMachineTypes.spec.js

Repository: gardener/dashboard

Length of output: 3281


Evaluate the computed value in this test to ensure the defaulting logic executes.

Line 134 creates the computed but never accesses it, so the mutation-sensitive logic inside the computed callback won't run. The test should assign and dereference .value to trigger the lazy-evaluated function.

🧪 Suggested fix
-        useFilteredMachineTypes(region, architecture)
+        const dashboardMachineTypes = useFilteredMachineTypes(region, architecture)
+        void dashboardMachineTypes.value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useFilteredMachineTypes(region, architecture)
const dashboardMachineTypes = useFilteredMachineTypes(region, architecture)
void dashboardMachineTypes.value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/__tests__/composables/useCloudProfile/useMachineTypes.spec.js` at
line 134, The test creates the computed from useFilteredMachineTypes(region,
architecture) but never evaluates it; change the test to capture the returned
computed (e.g., const filtered = useFilteredMachineTypes(region, architecture))
and then read or assign filtered.value so the computed callback runs and the
defaulting/mutation-sensitive logic executes; ensure any assertions use
filtered.value afterward to validate results.

Comment on lines +38 to +49
<template #selection="{ item }">
<span>{{ item.raw.title }}</span>
<v-chip
v-if="item.raw.namespaced"
size="x-small"
color="primary"
variant="tonal"
class="ml-2"
>
Namespaced
</v-chip>
</template>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Selection label fallback is bypassed in the custom slot.

If the selected value is not present in selectItems, item.raw.title can be empty, so the field renders a blank selection label.

💡 Suggested fix
-    <template `#selection`="{ item }">
-      <span>{{ item.raw.title }}</span>
+    <template `#selection`="{ item }">
+      <span>{{ item.title }}</span>
       <v-chip
-        v-if="item.raw.namespaced"
+        v-if="item.raw.namespaced ?? item.raw.kind === 'NamespacedCloudProfile'"
         size="x-small"
         color="primary"
         variant="tonal"
         class="ml-2"
       >
         Namespaced
       </v-chip>
     </template>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/GSelectCloudProfile.vue` around lines 38 - 49, The
custom selection slot currently renders item.raw.title directly which yields a
blank label when the selected value isn't in selectItems; update the selection
template (the template `#selection`="{ item }" slot) to use a safe fallback chain
(e.g. item.raw.title || item.title || item.text || item.value) so a meaningful
label is shown whenever item.raw.title is empty or undefined and preserve the
existing v-chip logic for item.raw.namespaced.

}

try {
const namespace = to.params.namespace ?? to.query.namespace
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize namespace to a single string before using it.

to.query.namespace may be string[]; forwarding it directly can cause invalid namespace checks and API calls.

💡 Suggested fix
-        const namespace = to.params.namespace ?? to.query.namespace
+        const rawNamespace = to.params.namespace ?? to.query.namespace
+        const namespace = Array.isArray(rawNamespace) ? rawNamespace[0] : rawNamespace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/router/guards.js` at line 73, The variable assigned to namespace
(currently using to.params.namespace ?? to.query.namespace) must be normalized
to a single string because to.query.namespace can be a string[]; update the
assignment in guards.js so that if to.query.namespace is an array you extract a
single string (e.g., first element) or join/flatten it, and then fallback to
to.params.namespace; ensure the final namespace is a string (use Array.isArray
on to.query.namespace and coerce/trim as needed) before any validation or API
calls that use namespace.

Comment on lines +69 to +75
async function fetchNamespacedCloudProfilesForNamespaces (namespaces = []) {
const namespacesToFetch = uniq(namespaces)
.filter(Boolean)
.filter(namespace => !hasNamespacedCloudProfilesForNamespace(namespace))

await Promise.all(namespacesToFetch.map(namespace => fetchNamespacedCloudProfiles(namespace)))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prune stale namespace cache entries when namespace scope shrinks.

Current logic only fetches missing namespaces; it never removes namespaces that are no longer in scope. That leaves stale namespaced cloud profiles in namespacedList / allCloudProfiles.

💡 Suggested fix
 async function fetchNamespacedCloudProfilesForNamespaces (namespaces = []) {
-  const namespacesToFetch = uniq(namespaces)
+  const allowedNamespaces = uniq(namespaces)
     .filter(Boolean)
-    .filter(namespace => !hasNamespacedCloudProfilesForNamespace(namespace))
+
+  // Drop cached namespaces that are no longer in scope
+  namespacedListsByNamespace.value = Object.fromEntries(
+    Object.entries(namespacedListsByNamespace.value)
+      .filter(([namespace]) => allowedNamespaces.includes(namespace)),
+  )
+  namespacedList.value = flatten(Object.values(namespacedListsByNamespace.value))
+
+  const namespacesToFetch = allowedNamespaces
+    .filter(namespace => !hasNamespacedCloudProfilesForNamespace(namespace))
 
   await Promise.all(namespacesToFetch.map(namespace => fetchNamespacedCloudProfiles(namespace)))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function fetchNamespacedCloudProfilesForNamespaces (namespaces = []) {
const namespacesToFetch = uniq(namespaces)
.filter(Boolean)
.filter(namespace => !hasNamespacedCloudProfilesForNamespace(namespace))
await Promise.all(namespacesToFetch.map(namespace => fetchNamespacedCloudProfiles(namespace)))
}
async function fetchNamespacedCloudProfilesForNamespaces (namespaces = []) {
const allowedNamespaces = uniq(namespaces)
.filter(Boolean)
// Drop cached namespaces that are no longer in scope
namespacedListsByNamespace.value = Object.fromEntries(
Object.entries(namespacedListsByNamespace.value)
.filter(([namespace]) => allowedNamespaces.includes(namespace)),
)
namespacedList.value = flatten(Object.values(namespacedListsByNamespace.value))
const namespacesToFetch = allowedNamespaces
.filter(namespace => !hasNamespacedCloudProfilesForNamespace(namespace))
await Promise.all(namespacesToFetch.map(namespace => fetchNamespacedCloudProfiles(namespace)))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/store/cloudProfile/index.js` around lines 69 - 75,
fetchNamespacedCloudProfilesForNamespaces currently only adds missing namespaces
but never removes stale entries, leaving namespacedList/allCloudProfiles
polluted; update this function to prune cache entries for namespaces that are no
longer in scope: compute the canonical set of requested namespaces
(uniq/filter(Boolean)), call the existing fetchNamespacedCloudProfiles for any
missing ones as before, and then remove any keys from namespacedList and any
entries in allCloudProfiles that belong to namespaces not in that canonical set
(use the store's mutation or helper that manages namespacedList and
allCloudProfiles so state stays consistent). Ensure you reference and update
namespacedList and allCloudProfiles within
fetchNamespacedCloudProfilesForNamespaces so removed namespaces are cleared
after the fetch step.

@klocke-io klocke-io changed the title feat(backend): add namespaced-cloud-profile-support feat: Add support for namespacedcloudprofiles Mar 3, 2026
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

use camel case for file like you did with the other files

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

b6bbcc0

done, thanks for catching it :)

@@ -50,6 +50,13 @@ rules:
verbs:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we also need to add this to gardener operator

Copy link
Copy Markdown
Member Author

@klocke-io klocke-io Mar 10, 2026

Choose a reason for hiding this comment

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

https://github.com/gardener/gardener/pull/13500/changes
Done and merged, or did you mean something else :)

const parentCloudProfile = getCloudProfile(parentName)

if (!parentCloudProfile) {
return null
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this case throw an exception?

Comment on lines +51 to +58
const result = _.cloneDeep(profile)
result.status = {
...result.status,
cloudProfileSpecDiff: diff || null,
}
delete result.status.cloudProfileSpec

return result
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
const result = _.cloneDeep(profile)
result.status = {
...result.status,
cloudProfileSpecDiff: diff || null,
}
delete result.status.cloudProfileSpec
return result
const { status = {}, ...profileRest } = profile
const { cloudProfileSpec, ...statusRest } = status
return {
...profileRest,
status: {
...statusRest,
cloudProfileSpecDiff: diff ?? null,
},
}

Comment thread backend/package.json
"test": "NODE_PATH=./dist cross-env NODE_ENV=test node --experimental-vm-modules $(yarn bin jest)",
"test-coverage": "yarn test --coverage"
"test-coverage": "yarn test --coverage",
"test-vitest": "vitest run"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

test-vitest is currently not run viamake verify, or?

const parentName = profile.spec?.parent?.name
const parent = find(list.value, ['metadata.name', parentName])
if (!parent) {
return profile
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isn't this an exception / should be handled as error case?

) ?? null
}
// Fallback: only return when name is unique across namespaces
const matches = filter(namespacedList.value, ['metadata.name', cloudProfileRef.name])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

when namespace is not set on cloudProfileRef, the namespace of the parent object (e.g. the shoot) has to be taken

Comment on lines +44 to +46
const isNamespacedInitial = computed(() => {
return Object.keys(namespacedListsByNamespace.value).length === 0
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

drop unused computed

@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented Mar 30, 2026

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@gardener-prow gardener-prow Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Mar 30, 2026
@gardener-ci-robot
Copy link
Copy Markdown
Contributor

The Gardener project currently lacks enough active contributors to adequately respond to all PRs.
This bot triages PRs according to the following rules:

  • After 30d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 14d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Mark this PR as active with /lifecycle active
  • Mark this PR as fresh with /remove-lifecycle stale
  • Mark this PR as rotten with /lifecycle rotten
  • Close this PR with /close

/lifecycle stale

@gardener-prow gardener-prow Bot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Apr 29, 2026
…tion

 Backend returns parentResourceVersion and cloudProfileSpecHash alongside diffs. Frontend verifies rehydrated specs against the hash, re-fetches drifted parents, and drops profiles that cannot be reconciled — surfacing errors to the user via the app store.
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: 2

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

Inline comments:
In `@backend/lib/services/namespacedCloudProfiles.js`:
- Line 44: The backend is producing spec hashes using a non-canonical JSON
serialization (backend/lib/utils/index.js#computeSpecHash) which mismatches the
frontend's canonicalized hash (frontend/src/utils/crypto.js#computeSpecHash) and
causes false profile drops; update the code that sets cloudProfileSpecHash (in
namespacedCloudProfiles.js) to canonicalize the spec before hashing—either
replace the current compute call with the same canonicalization routine used by
the frontend or add a stable/canonical JSON serialize step (e.g., stableSort
keys or use the shared canonicalize function) so computeSpecHash receives a
canonical string and both sides produce identical hashes.
- Around line 38-45: parentSpec is taken from simplifiedParent but
namespacedSpec is pulled raw from namespacedCloudProfile, causing inconsistent
inputs to jsondiffpatch.diff and computeSpecHash; fix by first producing a
simplified version of namespacedCloudProfile the same way simplifiedParent was
produced (so get the ['spec'] from that simplified object) and then use that
simplified namespaced spec in both jsondiffpatch.diff(parentSpec,
namespacedSpec) and computeSpecHash(namespacedSpec) so both diff and hash
operate on the same simplified representation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f03736d-2863-4505-b7bf-525133c19249

📥 Commits

Reviewing files that changed from the base of the PR and between b6bbcc0 and eef1d61.

⛔ Files ignored due to path filters (3)
  • .yarn/cache/canonicalize-npm-2.1.0-8ea3fe50f0-3b1ec61276.zip is excluded by !**/.yarn/**, !**/*.zip
  • backend/__tests__/acceptance/__snapshots__/api.cloudprofiles.spec.cjs.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (14)
  • .pnp.cjs
  • backend/__fixtures__/cloudprofiles.cjs
  • backend/__tests__/acceptance/api.cloudprofiles.spec.cjs
  • backend/__tests__/services.namespacedCloudProfiles.spec.js
  • backend/lib/routes/cloudprofiles.js
  • backend/lib/services/cloudprofiles.js
  • backend/lib/services/namespacedCloudProfiles.js
  • backend/lib/utils/index.js
  • backend/package.json
  • frontend/__tests__/stores/cloudProfile.spec.js
  • frontend/package.json
  • frontend/src/composables/useApi/api.js
  • frontend/src/store/cloudProfile/index.js
  • frontend/src/utils/crypto.js
✅ Files skipped from review due to trivial changes (3)
  • backend/fixtures/cloudprofiles.cjs
  • backend/tests/acceptance/api.cloudprofiles.spec.cjs
  • frontend/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/lib/utils/index.js
  • frontend/src/composables/useApi/api.js
  • backend/package.json

Comment on lines +38 to +45
const parentSpec = _.get(simplifiedParent, ['spec'], {})
const namespacedSpec = _.get(namespacedCloudProfile, ['status', 'cloudProfileSpec'], {})

return {
diff: jsondiffpatch.diff(parentSpec, namespacedSpec),
parentResourceVersion,
cloudProfileSpecHash: computeSpecHash(namespacedSpec),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Diff/hash input is inconsistent with the simplified parent spec.

On Line 38 the parent spec is simplified, but on Line 39 the namespaced spec is taken raw. That makes cloudProfileSpecDiff and cloudProfileSpecHash depend on unsimplified fields (notably providerConfig), which can create avoidable drift/noise.

💡 Suggested fix
-  const parentSpec = _.get(simplifiedParent, ['spec'], {})
-  const namespacedSpec = _.get(namespacedCloudProfile, ['status', 'cloudProfileSpec'], {})
+  const parentSpec = _.get(simplifiedParent, ['spec'], {})
+  const rawNamespacedSpec = _.get(namespacedCloudProfile, ['status', 'cloudProfileSpec'], {})
+  const namespacedSpec = _.get(
+    simplifyCloudProfile({ spec: rawNamespacedSpec }),
+    ['spec'],
+    {},
+  )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/services/namespacedCloudProfiles.js` around lines 38 - 45,
parentSpec is taken from simplifiedParent but namespacedSpec is pulled raw from
namespacedCloudProfile, causing inconsistent inputs to jsondiffpatch.diff and
computeSpecHash; fix by first producing a simplified version of
namespacedCloudProfile the same way simplifiedParent was produced (so get the
['spec'] from that simplified object) and then use that simplified namespaced
spec in both jsondiffpatch.diff(parentSpec, namespacedSpec) and
computeSpecHash(namespacedSpec) so both diff and hash operate on the same
simplified representation.

return {
diff: jsondiffpatch.diff(parentSpec, namespacedSpec),
parentResourceVersion,
cloudProfileSpecHash: computeSpecHash(namespacedSpec),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Backend/frontend spec-hash canonicalization is currently mismatched.

Line 44 relies on backend/lib/utils/index.js#computeSpecHash (JSON serialization order-sensitive), while the frontend rehydration path uses frontend/src/utils/crypto.js#computeSpecHash with canonicalization. Equal specs can hash differently and trigger false profile drops.

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

In `@backend/lib/services/namespacedCloudProfiles.js` at line 44, The backend is
producing spec hashes using a non-canonical JSON serialization
(backend/lib/utils/index.js#computeSpecHash) which mismatches the frontend's
canonicalized hash (frontend/src/utils/crypto.js#computeSpecHash) and causes
false profile drops; update the code that sets cloudProfileSpecHash (in
namespacedCloudProfiles.js) to canonicalize the spec before hashing—either
replace the current compute call with the same canonicalization routine used by
the frontend or add a stable/canonical JSON serialize step (e.g., stableSort
keys or use the shared canonicalize function) so computeSpecHash receives a
canonical string and both sides produce identical hashes.

@grolu grolu removed the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. needs/review Needs review needs/second-opinion Needs second review by someone else needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants