Skip to content

Commit c0109b2

Browse files
author
Alan Zhang
committed
folderUid fix for deprecated api
1 parent fc69d98 commit c0109b2

10 files changed

Lines changed: 413 additions & 50 deletions

File tree

‎src/amg/HISTORY.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,8 @@ Release History
172172
3.1.0
173173
++++++
174174
* Support dynamic dashboards schema
175+
176+
3.1.1
177+
++++++
178+
* `az grafana dashboard`, `az grafana restore`, `az grafana migrate`: use folder UIDs instead of numeric IDs to preserve dashboard and library panel folder placement
179+
* `az grafana restore`, `az grafana migrate`: preserve library panel folders when overwriting existing panels

‎src/amg/azext_amg/custom.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
from ._client_factory import cf_amg
2323
from .utils import get_yes_or_no_option, search_folders
2424
from .dashboard_v2 import (is_v2_dashboard_definition, require_dashboard_v2_api_version,
25-
create_dashboard_v2, resolve_dashboard_v2_api_version, read_dashboard)
25+
create_dashboard_v2, resolve_dashboard_v2_api_version, read_dashboard,
26+
dashboard_folder_uid)
2627

2728
logger = get_logger(__name__)
2829

@@ -384,8 +385,11 @@ def _create_dashboard(cmd, grafana_name, definition, title=None, folder_uid=None
384385
if title:
385386
payload['dashboard']['title'] = title
386387

387-
if folder_uid:
388+
if folder_uid is not None:
388389
payload['folderUid'] = folder_uid
390+
elif 'folderId' in payload:
391+
payload['folderUid'] = dashboard_folder_uid(payload)
392+
payload.pop('folderId', None)
389393

390394
payload['overwrite'] = overwrite or False
391395

@@ -435,8 +439,12 @@ def import_dashboard(cmd, grafana_name, definition, folder=None, resource_group_
435439
payload['dashboard'] = data
436440

437441
if folder:
438-
folder = _find_folder(cmd, resource_group_name, grafana_name, folder)
439-
payload['folderId'] = folder['id']
442+
folder = _find_folder(cmd, resource_group_name, grafana_name, folder,
443+
api_key_or_token=api_key_or_token)
444+
payload['folderUid'] = folder['uid']
445+
elif 'folderId' in payload:
446+
payload['folderUid'] = dashboard_folder_uid(payload)
447+
payload.pop('folderId', None)
440448

441449
payload['overwrite'] = overwrite or False
442450
payload['inputs'] = []

‎src/amg/azext_amg/dashboard_v2.py‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import copy
1212
import json
13+
import re
14+
from urllib.parse import urlsplit
1315

1416
import requests
1517

@@ -177,10 +179,43 @@ def dashboard_identity(content):
177179

178180

179181
def dashboard_folder_uid(content):
180-
# Return the folder uid for either a classic {meta, dashboard} object or a v2 resource.
182+
# Older backups identify folders by URL; newer dashboards and library panels expose a UID.
181183
if is_v2_dashboard_definition(content):
182184
return content.get("metadata", {}).get("annotations", {}).get("grafana.app/folder", "")
183-
return content.get("meta", {}).get("folderUid", "")
185+
186+
meta = content.get("meta", {})
187+
if not isinstance(meta, dict):
188+
raise ArgumentUsageError("The definition's 'meta' field must be an object.")
189+
190+
folder_uid = content.get("folderUid")
191+
if folder_uid is None:
192+
folder_uid = meta.get("folderUid")
193+
if folder_uid is not None:
194+
if not isinstance(folder_uid, str):
195+
raise ArgumentUsageError("The definition's 'folderUid' field must be a string.")
196+
return "" if folder_uid == "general" else folder_uid
197+
198+
folder_url = meta.get("folderUrl")
199+
if folder_url is not None and not isinstance(folder_url, str):
200+
raise ArgumentUsageError("The definition's 'folderUrl' field must be a string.")
201+
if folder_url:
202+
try:
203+
folder_path = urlsplit(folder_url).path
204+
except ValueError as ex:
205+
raise ArgumentUsageError("The definition's 'folderUrl' field must be a valid URL.") from ex
206+
match = re.search(r"(?:^|/)dashboards/f/([^/]+)(?:/|$)", folder_path)
207+
if match:
208+
folder_uid = match.group(1)
209+
return "" if folder_uid == "general" else folder_uid
210+
if folder_path.rstrip("/").split("/")[-1] == "dashboards":
211+
return ""
212+
raise ArgumentUsageError("The definition's 'folderUrl' does not identify a Grafana folder. "
213+
"Supply 'folderUid' instead.")
214+
215+
if content.get("folderId", meta.get("folderId")):
216+
raise ArgumentUsageError("The definition has a numeric 'folderId' but no folder UID. "
217+
"Supply 'folderUid' instead.")
218+
return ""
184219

185220

186221
def remap_v2_datasource_uids(node, uid_mapping):

‎src/amg/azext_amg/migrate.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ def migrate(backup_url, backup_headers, restore_url, restore_headers, dry_run,
5959
if 'general' in folders_to_include_set or 'general' not in folders_to_exclude_set:
6060
valid_folder_uids.add('')
6161

62-
# needs to be meta since in Grafana 8, the folderUid is in the meta.
63-
all_lib_panels_filtered = [p for p in all_source_lib_panels if p['meta']['folderUid'] in valid_folder_uids]
62+
all_lib_panels_filtered = [p for p in all_source_lib_panels if dashboard_folder_uid(p) in valid_folder_uids]
6463
(library_panels_created_summary,
6564
library_panels_overwrote_summary,
6665
dashboards_created_summary,

‎src/amg/azext_amg/restore.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
from azure.cli.core.style import print_styled_text, Style
1616
from knack.log import get_logger
1717

18-
from .utils import (get_folder_id, send_grafana_post, send_grafana_patch, send_grafana_put, send_grafana_delete,
18+
from .utils import (send_grafana_post, send_grafana_patch, send_grafana_put, send_grafana_delete,
1919
send_grafana_get, create_datasource_mapping, remap_datasource_uids, get_snapshot,
2020
search_annotations)
2121
from .dashboard_v2 import (is_v2_dashboard_definition, require_dashboard_v2_api_version,
22-
create_dashboard_v2, remap_v2_datasource_uids)
22+
create_dashboard_v2, remap_v2_datasource_uids, dashboard_folder_uid)
2323

2424
logger = get_logger(__name__)
2525

@@ -102,7 +102,7 @@ def create_dashboard(grafana_url, content, http_headers, overwrite):
102102

103103
payload = {
104104
'dashboard': content['dashboard'],
105-
'folderId': get_folder_id(content, grafana_url, http_post_headers=http_headers),
105+
'folderUid': dashboard_folder_uid(content),
106106
'overwrite': overwrite
107107
}
108108

@@ -166,8 +166,8 @@ def _load_and_create_library_panel(grafana_url, file_path, http_headers):
166166

167167

168168
def create_library_panel(grafana_url, payload, http_headers, overwrite):
169-
# set the folder id of the library panel
170-
payload['folderId'] = get_folder_id(payload, grafana_url, http_post_headers=http_headers)
169+
payload['folderUid'] = dashboard_folder_uid(payload)
170+
payload.pop('folderId', None)
171171

172172
datasources_missed = set()
173173
remap_datasource_uids(payload, uid_mapping, datasources_missed)
@@ -186,7 +186,8 @@ def create_library_panel(grafana_url, payload, http_headers, overwrite):
186186
'name': panel_name,
187187
'model': payload['model'],
188188
'version': content['result']['version'],
189-
'kind': payload['kind']
189+
'kind': payload['kind'],
190+
'folderUid': payload['folderUid']
190191
}
191192
(status, content) = send_grafana_patch(f'{grafana_url}/api/library-elements/{uid}',
192193
json.dumps(patch_payload), http_headers)

0 commit comments

Comments
 (0)