Skip to content

fix(grafana): discover App Platform API version instead of hardcoding v1 - #221

Merged
hyrious merged 2 commits into
oomol-lab:mainfrom
wengych:fix/grafana-api-version-discovery
Jul 29, 2026
Merged

fix(grafana): discover App Platform API version instead of hardcoding v1#221
hyrious merged 2 commits into
oomol-lab:mainfrom
wengych:fix/grafana-api-version-discovery

Conversation

@wengych

@wengych wengych commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

All nine App Platform-backed Grafana actions fail against Grafana 12.x with a Kubernetes-style 404:

the server could not find the requested resource

Affected: get_dashboard, create_dashboard, update_dashboard, delete_dashboard, list_folders, get_folder, create_folder, update_folder, delete_folder.

The six legacy-REST actions (search_dashboards and the five *_data_source actions) are unaffected, which makes this easy to misdiagnose: credential validation calls /api/org (legacy REST), so the connection reports configured: true and resolves the org profile correctly. The connection looks healthy and only fails once a dashboard/folder action is invoked.

Root cause

src/providers/grafana/runtime.ts hardcodes the API group version as v1:

function apiPath(input: Record<string, unknown>, resource: "folders" | "dashboards"): string {
  const namespace = optionalString(input.namespace) ?? defaultNamespace;
  const group = resource === "folders" ? "folder.grafana.app/v1" : "dashboard.grafana.app/v1";
  return `/apis/${group}/namespaces/${encodePathSegment(namespace)}/${resource}`;
}

Grafana's App Platform API groups are versioned per release, and v1 only became available in Grafana 13.0. Versions actually served (GET /apis/<group>):

Grafana dashboard.grafana.app folder.grafana.app
12.1 v1beta1 (preferred), v0alpha1, v2alpha1 v1beta1
12.4 v1beta1 (preferred), v0alpha1, v2beta1, v2alpha1 v1beta1
13.0+ includes v1 includes v1

Requesting an unserved version returns 404 rather than negotiating down, so every Grafana below 13.0 is affected. Reproduced identically on v1.1.0, v1.3.1 and latest.

Fix

Discover the served version at runtime instead of hardcoding it:

  • GET /apis/<group>, then pick the first match from a preference list.
  • Cache per baseUrl + group, so discovery costs one extra request per connection.
  • apiPath() becomes async and takes the request context; the nine call sites await it.
const grafanaApiVersionPreference = ["v1", "v1beta1", "v0alpha1"] as const;

Three deliberate choices:

  1. v1 is preferred, so Grafana 13+ keeps using the GA version — this is not a downgrade.
  2. Only the v1 lineage is listed. v2beta1/v2alpha1 use a different dashboard resource schema and are not interchangeable with what normalizeDashboard() expects.
  3. Discovery failure falls back to v1, preserving current behaviour for servers that do not expose the discovery endpoint.

Verification

A/B test with the same official image, the same Grafana 12.4 instance and the same service account token, differing only by the patched runtime.ts mounted over the original:

before   get_dashboard -> the server could not find the requested resource
         list_folders  -> the server could not find the requested resource

after    get_dashboard -> OK  (uid, title, folderUid, resourceVersion all returned)
         list_folders  -> OK  (folder list returned)

npm run fix-check and npm test (57 files, 563 tests) pass.

Notes

Only src/providers/grafana/runtime.ts changes — no action schemas, no catalog regeneration, no changes to the legacy-REST actions.

There are no existing Grafana tests in the repo, so this change is covered by the manual A/B verification above rather than by a unit test. Happy to add one against a mocked discovery endpoint if you'd like.

The nine App Platform-backed actions (dashboard/folder get, create, update,
delete and list_folders) hardcoded `v1` in the API group path. That version
only exists in Grafana 13.0+, so on every Grafana 12.x release the requests
returned a Kubernetes-style 404 ("the server could not find the requested
resource").

Discover the served version via `GET /apis/<group>` instead, picking the first
match from a preference list and caching the result per baseUrl and group.
`v1` stays first so Grafana 13+ keeps using the GA version. Only the v1 lineage
is considered: the v2 lineage uses a different dashboard resource schema and is
not interchangeable with what normalizeDashboard() expects. If discovery fails
the previous `v1` behaviour is kept.
Copilot AI review requested due to automatic review settings July 29, 2026 07:11
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes
    • Improved compatibility with Grafana App Platform by dynamically discovering the supported API version per App Platform group.
    • Added cached version resolution with best-effort discovery and a fallback to the default version when discovery fails.
    • Updated folder and dashboard operations to automatically use the correct discovered API version for requests.
  • Tests
    • Added coverage for version discovery, caching behavior, and retrying discovery after a failure.

Walkthrough

Grafana App Platform paths now resolve API versions dynamically through /apis/{group}. Successful resolutions are cached per base URL and API group, while discovery failures fall back to the default version without caching the fallback. Folder and dashboard handlers asynchronously use the resolved versions for execute-phase list, read, create, update, and delete requests.

Sequence Diagram(s)

sequenceDiagram
  participant GrafanaRuntime
  participant GrafanaAPI
  participant apiVersionCache
  GrafanaRuntime->>apiVersionCache: Check cached group version
  apiVersionCache-->>GrafanaRuntime: Return cached version when available
  GrafanaRuntime->>GrafanaAPI: Discover versions at /apis/{group}
  GrafanaAPI-->>GrafanaRuntime: Return served versions
  GrafanaRuntime->>apiVersionCache: Store selected version
  GrafanaRuntime->>GrafanaAPI: Execute folder or dashboard request
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required type(scope): subject format and clearly matches the Grafana API version discovery change.
Description check ✅ Passed The description is directly related to the Grafana API version discovery fix and its verification details.
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.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/providers/grafana/runtime.ts`:
- Line 476: The grafanaApiVersionCache update currently stores the fallback
version even when discovery fails. In the Grafana API version resolution flow,
only call grafanaApiVersionCache.set after a compatible version is successfully
discovered; leave the cache unchanged on errors or fallback results so later
requests can retry discovery.
🪄 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: 759259eb-f5dc-40b8-aab8-3d6652e5ad65

📥 Commits

Reviewing files that changed from the base of the PR and between 962d735 and 47aedb2.

📒 Files selected for processing (1)
  • src/providers/grafana/runtime.ts

Comment thread src/providers/grafana/runtime.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Grafana App Platform (Kubernetes-style) dashboard/folder actions against Grafana 12.x by discovering the served API group version at runtime (instead of hardcoding v1), and caching the resolved version per baseUrl + group to avoid repeated discovery requests.

Changes:

  • Add App Platform API group/version discovery (GET /apis/<group>) with a preferred-version list (v1, v1beta1, v0alpha1) and a per-connection cache.
  • Convert apiPath() to async and update the nine dashboard/folder action call sites to await the resolved API path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/providers/grafana/runtime.ts Outdated
Comment on lines +458 to +477
let resolved: string = grafanaApiVersionPreference[0];
try {
const payload = await grafanaRequestJson(`/apis/${group}`, { method: "GET" }, context);
const record = optionalRecord(payload) ?? {};
const served = new Set(
objectArrayOrEmpty(record.versions)
.map((entry) => optionalString(entry.version))
.filter((version): version is string => version !== undefined),
);
const match = grafanaApiVersionPreference.find((version) => served.has(version));
if (match !== undefined) {
resolved = match;
}
} catch {
// Discovery is best-effort. Falling back to the newest known version keeps the
// previous behaviour for servers that do not expose the discovery endpoint.
}

grafanaApiVersionCache.set(cacheKey, resolved);
return resolved;
// the v2 lineage uses a different resource schema and is not interchangeable here.
const grafanaApiVersionPreference = ["v1", "v1beta1", "v0alpha1"] as const;

const grafanaApiVersionCache = new Map<string, string>();
Comment on lines +448 to +456
async function resolveGrafanaApiVersion(
group: string,
context: GrafanaContext & { phase: GrafanaRequestPhase },
): Promise<string> {
const cacheKey = `${context.baseUrl}|${group}`;
const cached = grafanaApiVersionCache.get(cacheKey);
if (cached !== undefined) {
return cached;
}
@hyrious
hyrious merged commit 2871f11 into oomol-lab:main Jul 29, 2026
2 of 3 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/providers/grafana/runtime.test.ts (1)

25-54: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover cache-key isolation, not only cache hits.

This test only reuses one baseUrl and API group. A cache accidentally keyed only by group—or globally—would still pass. Add a second base URL with a different discovered version, and a second API group if available, to verify the ${baseUrl}|${group} contract in src/providers/grafana/runtime.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/grafana/runtime.test.ts` around lines 25 - 54, The Grafana
discovery test only verifies cache reuse, not cache isolation by base URL and
API group. Extend the test around grafanaActionHandlers.list_folders and its
discovery mock to perform discovery against a second baseUrl with a different
version, and use a second API group if the runtime exposes one, asserting
separate discovery requests and version-specific URLs according to the
`${baseUrl}|${group}` cache-key contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/providers/grafana/runtime.test.ts`:
- Around line 25-54: The Grafana discovery test only verifies cache reuse, not
cache isolation by base URL and API group. Extend the test around
grafanaActionHandlers.list_folders and its discovery mock to perform discovery
against a second baseUrl with a different version, and use a second API group if
the runtime exposes one, asserting separate discovery requests and
version-specific URLs according to the `${baseUrl}|${group}` cache-key contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e56012a-432e-4f73-9563-9c221aa8417e

📥 Commits

Reviewing files that changed from the base of the PR and between 47aedb2 and 683faed.

📒 Files selected for processing (2)
  • src/providers/grafana/runtime.test.ts
  • src/providers/grafana/runtime.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/providers/grafana/runtime.ts

l1shen pushed a commit that referenced this pull request Jul 31, 2026
Adds four read-only alerting actions to the Grafana provider:

| Action | Endpoint |
|---|---|
| `list_alert_rules` | `GET /api/v1/provisioning/alert-rules` |
| `get_alert_rule` | `GET /api/v1/provisioning/alert-rules/:uid` |
| `list_alert_instances` | `GET /api/alertmanager/grafana/api/v2/alerts`
(supports `active`/`silenced`/`inhibited` filters) |
| `list_contact_points` | `GET /api/v1/provisioning/contact-points` |

These endpoints belong to Grafana's stable legacy REST API and are not
affected by App Platform API version discovery (#221). This exact code
has been running against Grafana 12.4.3 in a private deployment for two
weeks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants