@@ -13,6 +13,24 @@ import {
1313const defaultNamespace = "default" ;
1414const grafanaDefaultRequestTimeoutMs = 30_000 ;
1515const folderParentAnnotation = "grafana.app/folder" ;
16+
17+ const grafanaAppApiGroups = {
18+ folders : "folder.grafana.app" ,
19+ dashboards : "dashboard.grafana.app" ,
20+ } as const ;
21+
22+ // Grafana's App Platform API groups are versioned and the set of served versions
23+ // differs per Grafana release, e.g.
24+ // Grafana 12.1 dashboard.grafana.app -> v1beta1, v0alpha1, v2alpha1
25+ // Grafana 12.4 dashboard.grafana.app -> v1beta1, v0alpha1, v2beta1, v2alpha1
26+ // Grafana 13.0 dashboard.grafana.app -> v1, ...
27+ // Requesting a version the server does not serve returns a Kubernetes-style 404
28+ // ("the server could not find the requested resource"), so the version has to be
29+ // discovered instead of hardcoded. Only versions from the v1 lineage are listed:
30+ // the v2 lineage uses a different resource schema and is not interchangeable here.
31+ const grafanaApiVersionPreference = [ "v1" , "v1beta1" , "v0alpha1" ] as const ;
32+
33+ const grafanaApiVersionCache = new Map < string , string > ( ) ;
1634const grafanaApiMetadataUrl = "https://grafana.com/docs/grafana/latest/developers/http_api/auth/#service-account-token" ;
1735
1836type GrafanaRequestPhase = "validate" | "execute" ;
@@ -134,7 +152,7 @@ async function executeListFolders(input: Record<string, unknown>, context: Grafa
134152 } ) ;
135153
136154 const payload = await grafanaRequestJson (
137- apiPath ( input , "folders" ) ,
155+ await apiPath ( input , "folders" , { ... context , phase : "execute" } ) ,
138156 { method : "GET" , query } ,
139157 {
140158 ...context ,
@@ -153,7 +171,7 @@ async function executeListFolders(input: Record<string, unknown>, context: Grafa
153171
154172async function executeGetFolder ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
155173 const payload = await grafanaRequestJson (
156- `${ apiPath ( input , "folders" ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
174+ `${ await apiPath ( input , "folders" , { ... context , phase : "execute" } ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
157175 { method : "GET" } ,
158176 { ...context , phase : "execute" } ,
159177 ) ;
@@ -162,7 +180,7 @@ async function executeGetFolder(input: Record<string, unknown>, context: Grafana
162180
163181async function executeCreateFolder ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
164182 const payload = await grafanaRequestJson (
165- apiPath ( input , "folders" ) ,
183+ await apiPath ( input , "folders" , { ... context , phase : "execute" } ) ,
166184 { method : "POST" , body : folderRequestBody ( input ) } ,
167185 { ...context , phase : "execute" } ,
168186 ) ;
@@ -172,7 +190,7 @@ async function executeCreateFolder(input: Record<string, unknown>, context: Graf
172190async function executeUpdateFolder ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
173191 const uid = requireString ( input . uid , "uid" ) ;
174192 const payload = await grafanaRequestJson (
175- `${ apiPath ( input , "folders" ) } /${ encodePathSegment ( uid ) } ` ,
193+ `${ await apiPath ( input , "folders" , { ... context , phase : "execute" } ) } /${ encodePathSegment ( uid ) } ` ,
176194 { method : "PUT" , body : folderRequestBody ( input , uid ) } ,
177195 { ...context , phase : "execute" } ,
178196 ) ;
@@ -181,7 +199,7 @@ async function executeUpdateFolder(input: Record<string, unknown>, context: Graf
181199
182200async function executeDeleteFolder ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
183201 const payload = await grafanaRequestJson (
184- `${ apiPath ( input , "folders" ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
202+ `${ await apiPath ( input , "folders" , { ... context , phase : "execute" } ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
185203 { method : "DELETE" } ,
186204 { ...context , phase : "execute" } ,
187205 ) ;
@@ -222,7 +240,7 @@ async function executeSearchDashboards(input: Record<string, unknown>, context:
222240
223241async function executeGetDashboard ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
224242 const payload = await grafanaRequestJson (
225- `${ apiPath ( input , "dashboards" ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
243+ `${ await apiPath ( input , "dashboards" , { ... context , phase : "execute" } ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
226244 { method : "GET" } ,
227245 { ...context , phase : "execute" } ,
228246 ) ;
@@ -231,7 +249,7 @@ async function executeGetDashboard(input: Record<string, unknown>, context: Graf
231249
232250async function executeCreateDashboard ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
233251 const payload = await grafanaRequestJson (
234- apiPath ( input , "dashboards" ) ,
252+ await apiPath ( input , "dashboards" , { ... context , phase : "execute" } ) ,
235253 { method : "POST" , body : dashboardRequestBody ( input ) } ,
236254 { ...context , phase : "execute" } ,
237255 ) ;
@@ -241,7 +259,7 @@ async function executeCreateDashboard(input: Record<string, unknown>, context: G
241259async function executeUpdateDashboard ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
242260 const uid = requireString ( input . uid , "uid" ) ;
243261 const payload = await grafanaRequestJson (
244- `${ apiPath ( input , "dashboards" ) } /${ encodePathSegment ( uid ) } ` ,
262+ `${ await apiPath ( input , "dashboards" , { ... context , phase : "execute" } ) } /${ encodePathSegment ( uid ) } ` ,
245263 { method : "PUT" , body : dashboardRequestBody ( input , uid ) } ,
246264 { ...context , phase : "execute" } ,
247265 ) ;
@@ -250,7 +268,7 @@ async function executeUpdateDashboard(input: Record<string, unknown>, context: G
250268
251269async function executeDeleteDashboard ( input : Record < string , unknown > , context : GrafanaContext ) : Promise < unknown > {
252270 const payload = await grafanaRequestJson (
253- `${ apiPath ( input , "dashboards" ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
271+ `${ await apiPath ( input , "dashboards" , { ... context , phase : "execute" } ) } /${ encodePathSegment ( requireString ( input . uid , "uid" ) ) } ` ,
254272 { method : "DELETE" } ,
255273 { ...context , phase : "execute" } ,
256274 ) ;
@@ -427,10 +445,47 @@ function extractGrafanaErrorMessage(payload: unknown): string | undefined {
427445 ) ;
428446}
429447
430- function apiPath ( input : Record < string , unknown > , resource : "folders" | "dashboards" ) : string {
448+ async function resolveGrafanaApiVersion (
449+ group : string ,
450+ context : GrafanaContext & { phase : GrafanaRequestPhase } ,
451+ ) : Promise < string > {
452+ const cacheKey = `${ context . baseUrl } |${ group } ` ;
453+ const cached = grafanaApiVersionCache . get ( cacheKey ) ;
454+ if ( cached !== undefined ) {
455+ return cached ;
456+ }
457+
458+ let resolved : string = grafanaApiVersionPreference [ 0 ] ;
459+ try {
460+ const payload = await grafanaRequestJson ( `/apis/${ group } ` , { method : "GET" } , context ) ;
461+ const record = optionalRecord ( payload ) ?? { } ;
462+ const served = new Set (
463+ objectArrayOrEmpty ( record . versions )
464+ . map ( ( entry ) => optionalString ( entry . version ) )
465+ . filter ( ( version ) : version is string => version !== undefined ) ,
466+ ) ;
467+ const match = grafanaApiVersionPreference . find ( ( version ) => served . has ( version ) ) ;
468+ if ( match !== undefined ) {
469+ resolved = match ;
470+ }
471+ } catch {
472+ // Discovery is best-effort. Falling back to the newest known version keeps the
473+ // previous behaviour for servers that do not expose the discovery endpoint.
474+ }
475+
476+ grafanaApiVersionCache . set ( cacheKey , resolved ) ;
477+ return resolved ;
478+ }
479+
480+ async function apiPath (
481+ input : Record < string , unknown > ,
482+ resource : "folders" | "dashboards" ,
483+ context : GrafanaContext & { phase : GrafanaRequestPhase } ,
484+ ) : Promise < string > {
431485 const namespace = optionalString ( input . namespace ) ?? defaultNamespace ;
432- const group = resource === "folders" ? "folder.grafana.app/v1" : "dashboard.grafana.app/v1" ;
433- return `/apis/${ group } /namespaces/${ encodePathSegment ( namespace ) } /${ resource } ` ;
486+ const group = grafanaAppApiGroups [ resource ] ;
487+ const version = await resolveGrafanaApiVersion ( group , context ) ;
488+ return `/apis/${ group } /${ version } /namespaces/${ encodePathSegment ( namespace ) } /${ resource } ` ;
434489}
435490
436491function folderRequestBody ( input : Record < string , unknown > , fallbackUid ?: string ) : Record < string , unknown > {
0 commit comments