Compare commits

..

2 Commits

Author SHA1 Message Date
Gabriel Mabille
2f71c8f562 More logs 2026-01-08 17:10:29 +01:00
Gabriel Mabille
d7a3d61726 Add debug logs, because I'm blind 2026-01-08 17:07:32 +01:00
110 changed files with 745 additions and 5804 deletions

View File

@@ -91,7 +91,6 @@ COPY pkg/storage/unified/resource pkg/storage/unified/resource
COPY pkg/storage/unified/resourcepb pkg/storage/unified/resourcepb
COPY pkg/storage/unified/apistore pkg/storage/unified/apistore
COPY pkg/semconv pkg/semconv
COPY pkg/plugins pkg/plugins
COPY pkg/aggregator pkg/aggregator
COPY apps/playlist apps/playlist
COPY apps/quotas apps/quotas

View File

@@ -1,287 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"panels": [
{
"datasource": "${datasource}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [10, 10],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 0
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true,
"refId": "B"
}
],
"title": "Number of Alloy Instances",
"type": "timeseries"
},
{
"datasource": "${datasource}",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percentunit"
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 0
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"hide": true,
"instant": false,
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
},
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "CPU usage",
"type": "timeseries"
}
],
"time": {
"from": "now-90m",
"to": "now"
},
"timezone": "utc",
"title": "Legacy DS Panel Query Ref",
"weekStart": ""
}
}

View File

@@ -1,294 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"panels": [
{
"datasource": "${datasource}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [
10,
10
],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 0
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true,
"refId": "B"
}
],
"title": "Number of Alloy Instances",
"type": "timeseries"
},
{
"datasource": "${datasource}",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percentunit"
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 0
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"hide": true,
"instant": false,
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
},
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "CPU usage",
"type": "timeseries"
}
],
"time": {
"from": "now-90m",
"to": "now"
},
"timezone": "utc",
"title": "Legacy DS Panel Query Ref",
"weekStart": ""
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,405 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v2alpha1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"query": {
"kind": "grafana",
"spec": {}
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"builtIn": true,
"legacyOptions": {
"type": "dashboard"
}
}
}
],
"cursorSync": "Off",
"editable": true,
"elements": {
"panel-16": {
"kind": "Panel",
"spec": {
"id": 16,
"title": "Number of Alloy Instances",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "",
"spec": {
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true
}
},
"datasource": {
"type": "",
"uid": "${datasource}"
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "timeseries",
"spec": {
"pluginVersion": "",
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [
10,
10
],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
}
}
}
}
},
"panel-17": {
"kind": "Panel",
"spec": {
"id": 17,
"title": "CPU usage",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "",
"spec": {
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"instant": false,
"legendFormat": "{{instance}}",
"range": true
}
},
"datasource": {
"type": "",
"uid": "${datasource}"
},
"refId": "A",
"hidden": true
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "",
"spec": {
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true
}
},
"datasource": {
"type": "",
"uid": "${datasource}"
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "timeseries",
"spec": {
"pluginVersion": "",
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-16"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-17"
}
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"timezone": "utc",
"from": "now-90m",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "Legacy DS Panel Query Ref",
"variables": []
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,411 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v2beta1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "grafana",
"version": "v0",
"datasource": {
"name": "-- Grafana --"
},
"spec": {}
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"builtIn": true,
"legacyOptions": {
"type": "dashboard"
}
}
}
],
"cursorSync": "Off",
"editable": true,
"elements": {
"panel-16": {
"kind": "Panel",
"spec": {
"id": 16,
"title": "Number of Alloy Instances",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "",
"version": "v0",
"datasource": {
"name": "${datasource}"
},
"spec": {
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true
}
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "timeseries",
"version": "",
"spec": {
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [
10,
10
],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
}
}
}
}
},
"panel-17": {
"kind": "Panel",
"spec": {
"id": 17,
"title": "CPU usage",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "",
"version": "v0",
"datasource": {
"name": "${datasource}"
},
"spec": {
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"instant": false,
"legendFormat": "{{instance}}",
"range": true
}
},
"refId": "A",
"hidden": true
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "",
"version": "v0",
"datasource": {
"name": "${datasource}"
},
"spec": {
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true
}
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "timeseries",
"version": "",
"spec": {
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-16"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-17"
}
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"timezone": "utc",
"from": "now-90m",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "Legacy DS Panel Query Ref",
"variables": []
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -88,11 +88,6 @@ func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard,
// Which means that we have schemaVersion: 42 dashboards where datasource variable references are still strings
normalizeTemplateVariableDatasources(out.Spec.Object)
// Normalize panel and target datasources from string to object format
// This handles legacy dashboards where panels/targets have datasource: "$datasource" (string)
// instead of datasource: { uid: "$datasource" } (object)
normalizePanelDatasources(out.Spec.Object)
return nil
}
@@ -139,62 +134,3 @@ func isTemplateVariableRef(s string) bool {
}
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
}
// normalizePanelDatasources converts panel and target string datasources to object format.
// Legacy dashboards may have panels/targets with datasource: "$datasource" (string).
// This normalizes them to datasource: { uid: "$datasource" } for consistent V1→V2 conversion.
func normalizePanelDatasources(dashboard map[string]interface{}) {
panels, ok := dashboard["panels"].([]interface{})
if !ok {
return
}
normalizePanelsDatasources(panels)
}
// normalizePanelsDatasources normalizes datasources in a list of panels (including nested row panels)
func normalizePanelsDatasources(panels []interface{}) {
for _, panel := range panels {
panelMap, ok := panel.(map[string]interface{})
if !ok {
continue
}
// Handle row panels with nested panels
if panelType, _ := panelMap["type"].(string); panelType == "row" {
if nestedPanels, ok := panelMap["panels"].([]interface{}); ok {
normalizePanelsDatasources(nestedPanels)
}
}
// Normalize panel-level datasource
if ds := panelMap["datasource"]; ds != nil {
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
panelMap["datasource"] = map[string]interface{}{
"uid": dsStr,
}
}
}
// Normalize target-level datasources
targets, ok := panelMap["targets"].([]interface{})
if !ok {
continue
}
for _, target := range targets {
targetMap, ok := target.(map[string]interface{})
if !ok {
continue
}
if ds := targetMap["datasource"]; ds != nil {
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
targetMap["datasource"] = map[string]interface{}{
"uid": dsStr,
}
}
}
}
}
}

View File

@@ -2059,12 +2059,6 @@ func transformPanelQueries(ctx context.Context, panelMap map[string]interface{},
Uid: &dsUID,
}
}
} else if dsStr, ok := ds.(string); ok && isTemplateVariable(dsStr) {
// Handle legacy panel datasource as string (template variable reference e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
panelDatasource = &dashv2alpha1.DashboardDataSourceRef{
Uid: &dsStr,
}
}
}
@@ -2151,10 +2145,6 @@ func transformSingleQuery(ctx context.Context, targetMap map[string]interface{},
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
queryDatasourceUID = resolveGrafanaDatasourceUID(queryDatasourceType, queryDatasourceUID)
}
} else if dsStr, ok := targetMap["datasource"].(string); ok && isTemplateVariable(dsStr) {
// Handle legacy target datasource as string (template variable reference e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
queryDatasourceUID = dsStr
}
// Use panel datasource if target datasource is missing or empty

View File

@@ -8,17 +8,12 @@ replace github.com/grafana/grafana/pkg/apimachinery => ../../pkg/apimachinery
replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver
replace github.com/grafana/grafana/pkg/plugins => ../../pkg/plugins
replace github.com/grafana/grafana/pkg/semconv => ../../pkg/semconv
require (
github.com/emicklei/go-restful/v3 v3.13.0
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7
github.com/grafana/grafana/pkg/apimachinery v0.0.0
github.com/grafana/grafana/pkg/plugins v0.0.0
github.com/stretchr/testify v1.11.1
k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.3
@@ -31,7 +26,7 @@ require (
cel.dev/expr v0.25.1 // indirect
github.com/Machiel/slugify v1.0.1 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
@@ -106,7 +101,7 @@ require (
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grafana/sqlds/v5 v5.0.3 // indirect

View File

@@ -11,8 +11,8 @@ github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -235,6 +235,8 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=

View File

@@ -116,26 +116,3 @@ type ConnectionList struct {
// +listType=atomic
Items []Connection `json:"items"`
}
// ExternalRepositoryList lists repositories from an external git provider
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ExternalRepositoryList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// +listType=atomic
Items []ExternalRepository `json:"items"`
}
type ExternalRepository struct {
// Name of the repository
Name string `json:"name"`
// Owner is the user, organization, or workspace that owns the repository
// For GitHub: organization or user
// For GitLab: namespace (user or group)
// For Bitbucket: workspace
// For pure Git: empty
Owner string `json:"owner,omitempty"`
// URL of the repository
URL string `json:"url"`
}

View File

@@ -197,7 +197,6 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
&HistoricJobList{},
&Connection{},
&ConnectionList{},
&ExternalRepositoryList{},
)
return nil
}

View File

@@ -262,53 +262,6 @@ func (in *ExportJobOptions) DeepCopy() *ExportJobOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalRepository) DeepCopyInto(out *ExternalRepository) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalRepository.
func (in *ExternalRepository) DeepCopy() *ExternalRepository {
if in == nil {
return nil
}
out := new(ExternalRepository)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalRepositoryList) DeepCopyInto(out *ExternalRepositoryList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ExternalRepository, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalRepositoryList.
func (in *ExternalRepositoryList) DeepCopy() *ExternalRepositoryList {
if in == nil {
return nil
}
out := new(ExternalRepositoryList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ExternalRepositoryList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FileItem) DeepCopyInto(out *FileItem) {
*out = *in

View File

@@ -26,8 +26,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.DeleteJobOptions": schema_pkg_apis_provisioning_v0alpha1_DeleteJobOptions(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository": schema_pkg_apis_provisioning_v0alpha1_ExternalRepository(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepositoryList": schema_pkg_apis_provisioning_v0alpha1_ExternalRepositoryList(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref),
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubConnectionConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubConnectionConfig(ref),
@@ -546,96 +544,6 @@ func schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref common.Reference
}
}
func schema_pkg_apis_provisioning_v0alpha1_ExternalRepository(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name of the repository",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"owner": {
SchemaProps: spec.SchemaProps{
Description: "Owner is the user, organization, or workspace that owns the repository For GitHub: organization or user For GitLab: namespace (user or group) For Bitbucket: workspace For pure Git: empty",
Type: []string{"string"},
Format: "",
},
},
"url": {
SchemaProps: spec.SchemaProps{
Description: "URL of the repository",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name", "url"},
},
},
}
}
func schema_pkg_apis_provisioning_v0alpha1_ExternalRepositoryList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "ExternalRepositoryList lists repositories from an external git provider",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_provisioning_v0alpha1_FileItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View File

@@ -1,7 +1,6 @@
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Paths
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Resources
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ExternalRepositoryList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,FileList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,HistoryList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Errors

View File

@@ -1,40 +0,0 @@
package controller
import (
"context"
"encoding/json"
"fmt"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// ConnectionStatusPatcher provides methods to patch Connection status subresources.
type ConnectionStatusPatcher struct {
client client.ProvisioningV0alpha1Interface
}
// NewConnectionStatusPatcher creates a new ConnectionStatusPatcher.
func NewConnectionStatusPatcher(client client.ProvisioningV0alpha1Interface) *ConnectionStatusPatcher {
return &ConnectionStatusPatcher{
client: client,
}
}
// Patch applies JSON patch operations to a Connection's status subresource.
func (p *ConnectionStatusPatcher) Patch(ctx context.Context, conn *provisioning.Connection, patchOperations ...map[string]interface{}) error {
patch, err := json.Marshal(patchOperations)
if err != nil {
return fmt.Errorf("unable to marshal patch data: %w", err)
}
_, err = p.client.Connections(conn.Namespace).
Patch(ctx, conn.Name, types.JSONPatchType, patch, metav1.PatchOptions{}, "status")
if err != nil {
return fmt.Errorf("unable to update connection status: %w", err)
}
return nil
}

View File

@@ -2234,8 +2234,6 @@ encryption_provider = secret_key.v1
# These flags are required in on-prem installations for GitSync to work
#
# Whether to register the MT CRUD API
register_api_server = true
# Whether to create the MT secrets management database
run_secrets_db_migrations = true
# Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true.

View File

@@ -2123,8 +2123,6 @@ default_datasource_uid =
# These flags are required in on-prem installations for GitSync to work
#
# Whether to register the MT CRUD API
;register_api_server = true
# Whether to create the MT secrets management database
;run_secrets_db_migrations = true
# Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true.

View File

@@ -186,7 +186,7 @@ For the JSON and field usage notes, refer to the [links schema documentation](ht
### `tags`
Tags associated with the dashboard. Each tag can be up to 50 characters long.
The tags associated with the dashboard:
` [...string]`

View File

@@ -25,7 +25,7 @@ Keys:
- **theme** - One of: `light`, `dark`, or an empty string for the default theme
- **homeDashboardId** - Deprecated. Use `homeDashboardUID` instead.
- **homeDashboardUID**: The `:uid` of a dashboard
- **timezone** - Any valid IANA timezone string (e.g., `America/New_York`, `Europe/London`), `utc`, `browser`, or an empty string for the default.
- **timezone** - One of: `utc`, `browser`, or an empty string for the default
Omitting a key will cause the current value to be replaced with the
system default value.

10
go.mod
View File

@@ -25,6 +25,7 @@ require (
github.com/Masterminds/semver v1.5.0 // @grafana/grafana-backend-group
github.com/Masterminds/semver/v3 v3.4.0 // @grafana/grafana-developer-enablement-squad
github.com/Masterminds/sprig/v3 v3.3.0 // @grafana/grafana-backend-group
github.com/ProtonMail/go-crypto v1.1.6 // @grafana/plugins-platform-backend
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/grafana-backend-group
github.com/alicebob/miniredis/v2 v2.34.0 // @grafana/alerting-backend
github.com/andybalholm/brotli v1.2.0 // @grafana/partner-datasources
@@ -119,7 +120,8 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // @grafana/identity-access-team
github.com/hashicorp/go-hclog v1.6.3 // @grafana/plugins-platform-backend
github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad
github.com/hashicorp/go-plugin v1.7.0 // indirect; @grafana/plugins-platform-backend
github.com/hashicorp/go-plugin v1.7.0 // @grafana/plugins-platform-backend
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 // @grafana/plugins-platform-backend
github.com/hashicorp/go-version v1.7.0 // @grafana/grafana-backend-group
github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-backend
github.com/hashicorp/hcl/v2 v2.24.0 // @grafana/alerting-backend
@@ -391,6 +393,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
@@ -487,6 +490,7 @@ require (
github.com/jhump/protoreflect v1.17.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
@@ -654,8 +658,10 @@ require (
require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
require github.com/Machiel/slugify v1.0.1 // @grafana/plugins-platform-backend
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect

15
go.sum
View File

@@ -679,7 +679,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@@ -737,6 +738,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U=
github.com/IBM/pgxpoolprometheus v1.1.2/go.mod h1:+vWzISN6S9ssgurhUNmm6AlXL9XLah3TdWJktquKTR8=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -759,6 +762,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -1026,6 +1031,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -1753,6 +1760,8 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8=
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
@@ -1877,6 +1886,10 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=

View File

@@ -32,7 +32,6 @@ use (
./pkg/build
./pkg/build/wire // skip:golangci-lint
./pkg/codegen
./pkg/plugins
./pkg/plugins/codegen
./pkg/promlib
./pkg/semconv

View File

@@ -280,7 +280,6 @@ github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
@@ -575,7 +574,6 @@ github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnx
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY=
github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
@@ -1351,7 +1349,6 @@ github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusrec
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.121.0/go.mod h1:3axnebi8xUm9ifbs1myzehw2nODtIMrQlL566sJ4bYw=
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.124.1 h1:XkxqUEoukMWXF+EpEWeM9itXKt62yKi13Lzd8ZEASP4=
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.124.1/go.mod h1:CuCZVPz+yn88b5vhZPAlxaMrVuhAVexUV6f8b07lpUc=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
@@ -1911,6 +1908,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
@@ -1954,12 +1952,10 @@ gocloud.dev/secrets/hashivault v0.42.0/go.mod h1:LXprr1XLEAT7BVZ+Y66dJEHQMzDsowI
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
@@ -2067,7 +2063,6 @@ golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
@@ -2092,6 +2087,7 @@ golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
@@ -2241,6 +2237,7 @@ gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzE
gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4=
gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34=
honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw=

View File

@@ -6,7 +6,6 @@
"version": "12.4.0-pre",
"repository": "github:grafana/grafana",
"scripts": {
"analytics-report": "node --experimental-strip-types ./scripts/cli/analytics/main.mts",
"check-frontend-dev": "./scripts/check-frontend-dev.sh",
"build": "NODE_ENV=production nx exec --verbose -- webpack --config scripts/webpack/webpack.prod.js",
"build:nominify": "yarn run build -- --env noMinify=1",
@@ -431,7 +430,6 @@
"swagger-ui-react": "5.30.3",
"symbol-observable": "4.0.0",
"systemjs": "6.15.1",
"ts-morph": "^27.0.2",
"tslib": "2.8.1",
"tween-functions": "^1.2.0",
"type-fest": "^4.18.2",

View File

@@ -96,7 +96,7 @@
"@faker-js/faker": "^9.8.0",
"@grafana/api-clients": "12.4.0-pre",
"@grafana/i18n": "12.4.0-pre",
"@reduxjs/toolkit": "2.10.1",
"@reduxjs/toolkit": "^2.9.0",
"fishery": "^2.3.1",
"lodash": "^4.17.21",
"tinycolor2": "^1.6.0"

View File

@@ -170,7 +170,7 @@
},
"peerDependencies": {
"@grafana/runtime": ">=11.6 <= 12.x",
"@reduxjs/toolkit": "^2.10.0",
"@reduxjs/toolkit": "^2.8.0",
"rxjs": "7.8.2"
}
}

View File

@@ -5312,8 +5312,7 @@ export type PatchPrefsCmd = {
queryHistory?: QueryHistoryPreference;
regionalFormat?: string;
theme?: 'light' | 'dark';
/** Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string */
timezone?: string;
timezone?: 'utc' | 'browser';
weekStart?: string;
};
export type UpdatePrefsCmd = {
@@ -5326,8 +5325,7 @@ export type UpdatePrefsCmd = {
queryHistory?: QueryHistoryPreference;
regionalFormat?: string;
theme?: 'light' | 'dark' | 'system';
/** Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string */
timezone?: string;
timezone?: 'utc' | 'browser';
weekStart?: string;
};
export type OrgUserDto = {

View File

@@ -86,8 +86,7 @@ export type PatchPrefsCmd = {
queryHistory?: QueryHistoryPreference;
regionalFormat?: string;
theme?: 'light' | 'dark';
/** Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string */
timezone?: string;
timezone?: 'utc' | 'browser';
weekStart?: string;
};
export type UpdatePrefsCmd = {
@@ -100,8 +99,7 @@ export type UpdatePrefsCmd = {
queryHistory?: QueryHistoryPreference;
regionalFormat?: string;
theme?: 'light' | 'dark' | 'system';
/** Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string */
timezone?: string;
timezone?: 'utc' | 'browser';
weekStart?: string;
};
export const {

View File

@@ -122,10 +122,6 @@ const injectedRtkApi = api
}),
invalidatesTags: ['Connection'],
}),
getConnectionRepositories: build.query<GetConnectionRepositoriesApiResponse, GetConnectionRepositoriesApiArg>({
query: (queryArg) => ({ url: `/connections/${queryArg.name}/repositories` }),
providesTags: ['Connection'],
}),
getConnectionStatus: build.query<GetConnectionStatusApiResponse, GetConnectionStatusApiArg>({
query: (queryArg) => ({
url: `/connections/${queryArg.name}/status`,
@@ -730,18 +726,6 @@ export type UpdateConnectionApiArg = {
force?: boolean;
patch: Patch;
};
export type GetConnectionRepositoriesApiResponse = /** status 200 OK */ {
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
apiVersion?: string;
items: any[];
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
metadata?: any;
};
export type GetConnectionRepositoriesApiArg = {
/** name of the ExternalRepositoryList */
name: string;
};
export type GetConnectionStatusApiResponse = /** status 200 OK */ Connection;
export type GetConnectionStatusApiArg = {
/** name of the Connection */
@@ -2095,8 +2079,6 @@ export const {
useReplaceConnectionMutation,
useDeleteConnectionMutation,
useUpdateConnectionMutation,
useGetConnectionRepositoriesQuery,
useLazyGetConnectionRepositoriesQuery,
useGetConnectionStatusQuery,
useLazyGetConnectionStatusQuery,
useReplaceConnectionStatusMutation,

View File

@@ -181,24 +181,3 @@ When a violation is detected, the rule reports:
```
Import '../status-history/utils' reaches outside the 'histogram' plugin directory. Plugins should only import from external dependencies or relative paths within their own directory.
```
### `tracking-event-creation`
Checks that the process to create a tracking event is followed in the right way.
#### `eventFactoryLiterals`
Check if the values passed to `createEventFactory` are literals.
```tsx
// Bad ❌
const repo = 'grafana';
const createUnifiedHistoryEvent = createEventFactory(repo, 'unified_history');
// Bad ❌
const history = 'history';
const createUnifiedHistoryEvent = createEventFactory('grafana', `unified_${history}`);
// Good ✅
const createUnifiedHistoryEvent = createEventFactory('grafana', 'unified_history');
```

View File

@@ -5,7 +5,6 @@ const themeTokenUsage = require('./rules/theme-token-usage.cjs');
const noRestrictedImgSrcs = require('./rules/no-restricted-img-srcs.cjs');
const consistentStoryTitles = require('./rules/consistent-story-titles.cjs');
const noPluginExternalImportPaths = require('./rules/no-plugin-external-import-paths.cjs');
const trackingEventCreation = require('./rules/tracking-event-creation.cjs');
module.exports = {
rules: {
@@ -16,6 +15,5 @@ module.exports = {
'no-restricted-img-srcs': noRestrictedImgSrcs,
'consistent-story-titles': consistentStoryTitles,
'no-plugin-external-import-paths': noPluginExternalImportPaths,
'tracking-event-creation': trackingEventCreation,
},
};

View File

@@ -1,158 +0,0 @@
// @ts-check
const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils');
const createRule = ESLintUtils.RuleCreator(
(name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`
);
const trackingEventCreation = createRule({
create(context) {
// Track what name createEventFactory is imported as
let createEventFactoryName = 'createEventFactory';
// Track if createEventFactory is imported
let isCreateEventFactoryImported = false;
// Track variables that store createEventFactory calls
const eventFactoryVariables = new Set();
return {
ImportSpecifier(node) {
if (node.imported.type === AST_NODE_TYPES.Identifier && node.imported.name === 'createEventFactory') {
// Remember what name it was imported as (handles aliased imports)
createEventFactoryName = node.local.name;
isCreateEventFactoryImported = true;
}
},
VariableDeclarator(node) {
if (!isCreateEventFactoryImported) {
return;
}
// Track variables initialized with createEventFactory calls
if (
node.init?.type === AST_NODE_TYPES.CallExpression &&
node.init.callee.type === AST_NODE_TYPES.Identifier &&
node.init.callee.name === createEventFactoryName
) {
const variableName = node.id.type === AST_NODE_TYPES.Identifier && node.id.name;
if (variableName) {
eventFactoryVariables.add(variableName);
}
// Check if arguments are literals
const args = node.init.arguments;
const argsAreNotLiterals = args.some((arg) => arg.type !== AST_NODE_TYPES.Literal);
if (argsAreNotLiterals) {
return context.report({
node: node.init,
messageId: 'eventFactoryLiterals',
});
}
}
},
ExportNamedDeclaration(node) {
if (!isCreateEventFactoryImported) {
return;
}
if (
node.declaration?.type === AST_NODE_TYPES.VariableDeclaration &&
node.declaration.declarations[0].init?.type === AST_NODE_TYPES.CallExpression
) {
const callee = node.declaration.declarations[0].init.callee;
if (callee.type === AST_NODE_TYPES.Identifier && eventFactoryVariables.has(callee.name)) {
// Check for comments
// Check for comments
const comments = context.sourceCode.getCommentsBefore(node);
if (!comments || comments.length === 0) {
return context.report({
node,
messageId: 'missingFunctionComment',
});
}
const jsDocComment = comments.find((comment) => comment.value.slice(0, 1) === '*');
if (!jsDocComment) {
return context.report({
node,
messageId: 'missingJsDocComment',
});
}
}
}
},
TSInterfaceDeclaration(node) {
if (!isCreateEventFactoryImported) {
return;
}
// Check if interface extends TrackingEvent
let extendsTrackingEvent = false;
if (node.extends && node.extends.length > 0) {
const interfaceExtends = node.extends;
extendsTrackingEvent = interfaceExtends.some((extend) => {
return (
extend.expression.type === AST_NODE_TYPES.Identifier && extend.expression.name === 'TrackingEventProps'
);
});
}
if (!node.extends || !extendsTrackingEvent) {
return context.report({
node,
messageId: 'interfaceMustExtend',
});
}
//Check if the interface properties has comments
if (node.body.type === AST_NODE_TYPES.TSInterfaceBody) {
const properties = node.body.body;
properties.forEach((property) => {
const comments = context.sourceCode.getCommentsBefore(property);
if (!comments || comments.length === 0) {
return context.report({
node: property,
messageId: 'missingPropertyComment',
});
}
const jsDocComment = comments.find((comment) => comment.value.slice(0, 1) === '*');
if (!jsDocComment) {
return context.report({
node: property,
messageId: 'missingJsDocComment',
});
}
});
}
},
TSTypeAliasDeclaration(node) {
if (!isCreateEventFactoryImported) {
return;
}
// Check if types has comments
const comments = context.sourceCode.getCommentsBefore(node);
if (!comments || comments.length === 0) {
return context.report({
node,
messageId: 'missingPropertyComment',
});
}
},
};
},
name: 'tracking-event-creation',
meta: {
type: 'problem',
docs: {
description: 'Check that the tracking event is created in the right way',
},
messages: {
eventFactoryLiterals: 'Params passed to `createEventFactory` must be literals',
missingFunctionComment: 'Event function needs to have a description of its purpose',
missingPropertyComment: 'Event property needs to have a description of its purpose',
interfaceMustExtend: 'Interface must extend `TrackingEvent`',
missingJsDocComment: 'Comment needs to be a jsDoc comment (begin comment with `*`)',
},
schema: [],
},
defaultOptions: [],
});
module.exports = trackingEventCreation;

View File

@@ -54,7 +54,6 @@ export const TagsInput = forwardRef<HTMLInputElement, Props>(
const [newTagName, setNewTagName] = useState('');
const styles = useStyles2(getStyles);
const theme = useTheme2();
const isTagTooLong = newTagName.length > 50;
const onNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setNewTagName(event.target.value);
@@ -66,9 +65,6 @@ export const TagsInput = forwardRef<HTMLInputElement, Props>(
const onAdd = (event?: React.MouseEvent | React.KeyboardEvent) => {
event?.preventDefault();
if (newTagName.length > 50) {
return;
}
if (!tags.includes(newTagName)) {
onChange(tags.concat(newTagName));
}
@@ -98,17 +94,14 @@ export const TagsInput = forwardRef<HTMLInputElement, Props>(
value={newTagName}
onKeyDown={onKeyboardAdd}
onBlur={onBlur}
invalid={invalid || isTagTooLong}
invalid={invalid}
suffix={
<Button
fill="text"
className={styles.addButtonStyle}
onClick={onAdd}
size="md"
disabled={newTagName.length <= 0 || isTagTooLong}
title={
isTagTooLong ? t('grafana-ui.tags-input.tag-too-long', 'Tag too long, max 50 characters') : undefined
}
disabled={newTagName.length <= 0}
>
<Trans i18nKey="grafana-ui.tags-input.add">Add</Trans>
</Button>

View File

@@ -13,7 +13,7 @@ type UpdatePrefsCmd struct {
// Deprecated: Use HomeDashboardUID instead
HomeDashboardID int64 `json:"homeDashboardId"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
// Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string
// Enum: utc,browser
Timezone string `json:"timezone"`
WeekStart string `json:"weekStart"`
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
@@ -31,7 +31,7 @@ type PatchPrefsCmd struct {
// Default:0
// Deprecated: Use HomeDashboardUID instead
HomeDashboardID *int64 `json:"homeDashboardId,omitempty"`
// Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string
// Enum: utc,browser
Timezone *string `json:"timezone,omitempty"`
WeekStart *string `json:"weekStart,omitempty"`
Language *string `json:"language,omitempty"`

View File

@@ -134,10 +134,6 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
}
if dtoCmd.Timezone != nil && !pref.IsValidTimezone(*dtoCmd.Timezone) {
return response.Error(http.StatusBadRequest, "Invalid timezone. Must be a valid IANA timezone (e.g., America/New_York), 'utc', 'browser', or empty string", nil)
}
// convert dashboard UID to ID in order to store internally if it exists in the query, otherwise take the id from query
// nolint:staticcheck
dashboardID := dtoCmd.HomeDashboardID

View File

@@ -36,6 +36,7 @@ import (
type provisioningControllerConfig struct {
provisioningClient *client.Clientset
resyncInterval time.Duration
repoFactory repository.Factory
unified resources.ResourceStore
clients resources.ClientFactory
tokenExchangeClient *authn.TokenExchangeClient
@@ -128,6 +129,16 @@ func setupFromConfig(cfg *setting.Cfg, registry prometheus.Registerer) (controll
return nil, fmt.Errorf("failed to create provisioning client: %w", err)
}
decrypter, err := setupDecrypter(cfg, tracer, tokenExchangeClient)
if err != nil {
return nil, fmt.Errorf("failed to setup decrypter: %w", err)
}
repoFactory, err := setupRepoFactory(cfg, decrypter, provisioningClient, registry)
if err != nil {
return nil, fmt.Errorf("failed to setup repository getter: %w", err)
}
// HACK: This logic directly connects to unified storage. We are doing this for now as there is no global
// search endpoint. But controllers, in general, should not connect directly to unified storage and instead
// go through the api server. Once there is a global search endpoint, we will switch to that here as well.
@@ -184,6 +195,7 @@ func setupFromConfig(cfg *setting.Cfg, registry prometheus.Registerer) (controll
return &provisioningControllerConfig{
provisioningClient: provisioningClient,
repoFactory: repoFactory,
unified: unified,
clients: clients,
resyncInterval: operatorSec.Key("resync_interval").MustDuration(60 * time.Second),

View File

@@ -1,86 +0,0 @@
package provisioning
import (
"context"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/client-go/tools/cache"
appcontroller "github.com/grafana/grafana/apps/provisioning/pkg/controller"
informer "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/setting"
)
// RunConnectionController starts the connection controller operator.
func RunConnectionController(deps server.OperatorDependencies) error {
logger := logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})).With("logger", "provisioning-connection-controller")
logger.Info("Starting provisioning connection controller")
controllerCfg, err := getConnectionControllerConfig(deps.Config, deps.Registerer)
if err != nil {
return fmt.Errorf("failed to setup operator: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("Received shutdown signal, stopping controllers")
cancel()
}()
informerFactory := informer.NewSharedInformerFactoryWithOptions(
controllerCfg.provisioningClient,
controllerCfg.resyncInterval,
)
statusPatcher := appcontroller.NewConnectionStatusPatcher(controllerCfg.provisioningClient.ProvisioningV0alpha1())
connInformer := informerFactory.Provisioning().V0alpha1().Connections()
connController, err := controller.NewConnectionController(
controllerCfg.provisioningClient.ProvisioningV0alpha1(),
connInformer,
statusPatcher,
)
if err != nil {
return fmt.Errorf("failed to create connection controller: %w", err)
}
informerFactory.Start(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), connInformer.Informer().HasSynced) {
return fmt.Errorf("failed to sync informer cache")
}
connController.Run(ctx, controllerCfg.workerCount)
return nil
}
type connectionControllerConfig struct {
provisioningControllerConfig
workerCount int
}
func getConnectionControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (*connectionControllerConfig, error) {
controllerCfg, err := setupFromConfig(cfg, registry)
if err != nil {
return nil, err
}
return &connectionControllerConfig{
provisioningControllerConfig: *controllerCfg,
workerCount: cfg.SectionWithEnvOverrides("operator").Key("worker_count").MustInt(1),
}, nil
}

View File

@@ -106,7 +106,6 @@ func RunRepoController(deps server.OperatorDependencies) error {
type repoControllerConfig struct {
provisioningControllerConfig
repoFactory repository.Factory
workerCount int
parallelOperations int
allowedTargets []string
@@ -120,17 +119,6 @@ func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (
return nil, err
}
// Setup repository factory for repo controller
decrypter, err := setupDecrypter(cfg, tracing.NewNoopTracerService(), controllerCfg.tokenExchangeClient)
if err != nil {
return nil, fmt.Errorf("failed to setup decrypter: %w", err)
}
repoFactory, err := setupRepoFactory(cfg, decrypter, controllerCfg.provisioningClient, registry)
if err != nil {
return nil, fmt.Errorf("failed to setup repository factory: %w", err)
}
allowedTargets := []string{}
cfg.SectionWithEnvOverrides("provisioning").Key("allowed_targets").Strings("|")
if len(allowedTargets) == 0 {
@@ -139,7 +127,6 @@ func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (
return &repoControllerConfig{
provisioningControllerConfig: *controllerCfg,
repoFactory: repoFactory,
allowedTargets: allowedTargets,
workerCount: cfg.SectionWithEnvOverrides("operator").Key("worker_count").MustInt(1),
parallelOperations: cfg.SectionWithEnvOverrides("operator").Key("parallel_operations").MustInt(10),

View File

@@ -13,12 +13,6 @@ func init() {
RunFunc: provisioning.RunRepoController,
})
server.RegisterOperator(server.Operator{
Name: "provisioning-connection",
Description: "Watch provisioning connections",
RunFunc: provisioning.RunConnectionController,
})
server.RegisterOperator(server.Operator{
Name: "iam-folder-reconciler",
Description: "Reconcile folder resources into Zanzana",

View File

@@ -1,130 +0,0 @@
module github.com/grafana/grafana/pkg/plugins
go 1.25.5
require (
github.com/Machiel/slugify v1.0.1
github.com/ProtonMail/go-crypto v1.3.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
github.com/grafana/grafana-plugin-sdk-go v0.284.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0
github.com/grafana/grafana/pkg/semconv v0.0.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.7.0
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.11
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
k8s.io/apimachinery v0.34.3 // indirect
k8s.io/apiserver v0.34.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
)
replace (
github.com/grafana/grafana/pkg/apimachinery => ../apimachinery
github.com/grafana/grafana/pkg/semconv => ../semconv
)

View File

@@ -1,347 +0,0 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4=
github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E=
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8=
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE=
github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 h1:oPW/SRFyHgIgxrvNhSBzqvZER2N5kRlci3/rGTOuyWo=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bnmZNO6gBbBta6nohD/1Z+f9waH2oXyBs=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -389,11 +389,6 @@ func (b *DashboardsAPIBuilder) validateCreate(ctx context.Context, a admission.A
return apierrors.NewBadRequest(err.Error())
}
// Validate tags
if err := validateDashboardTags(dashObj); err != nil {
return apierrors.NewBadRequest(err.Error())
}
id, err := identity.GetRequester(ctx)
if err != nil {
return fmt.Errorf("error getting requester: %w", err)
@@ -464,11 +459,6 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A
return apierrors.NewBadRequest(err.Error())
}
// Validate tags
if err := validateDashboardTags(newDashObj); err != nil {
return apierrors.NewBadRequest(err.Error())
}
// Validate folder existence if specified and changed
if !a.IsDryRun() && newAccessor.GetFolder() != oldAccessor.GetFolder() && newAccessor.GetFolder() != "" {
id, err := identity.GetRequester(ctx)
@@ -566,32 +556,6 @@ func getDashboardProperties(obj runtime.Object) (string, string, error) {
return title, refresh, nil
}
// validateDashboardTags validates that all dashboard tags are within the maximum length
func validateDashboardTags(obj runtime.Object) error {
var tags []string
switch d := obj.(type) {
case *dashv0.Dashboard:
tags = d.Spec.GetNestedStringSlice("tags")
case *dashv1.Dashboard:
tags = d.Spec.GetNestedStringSlice("tags")
case *dashv2alpha1.Dashboard:
tags = d.Spec.Tags
case *dashv2beta1.Dashboard:
tags = d.Spec.Tags
default:
return fmt.Errorf("unsupported dashboard version: %T", obj)
}
for _, tag := range tags {
if len(tag) > 50 {
return dashboards.ErrDashboardTagTooLong
}
}
return nil
}
func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
storageOpts := apistore.StorageOptions{
EnableFolderSupport: true,

View File

@@ -170,9 +170,17 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
if !ok {
return nil, storewrapper.ErrUnauthenticated
}
r.logger.Debug("filtering resource permissions list with auth info",
"namespace", authInfo.GetNamespace(),
"identity Subject", authInfo.GetSubject(),
"identity UID", authInfo.GetUID(),
"identity type", authInfo.GetIdentityType(),
)
switch l := list.(type) {
case *iamv0.ResourcePermissionList:
r.logger.Debug("filtering list of length", "length", len(l.Items))
var (
filteredItems []iamv0.ResourcePermission
err error
@@ -182,6 +190,12 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
target := item.Spec.Resource
targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource}
r.logger.Debug("target resource",
"group", target.ApiGroup,
"resource", target.Resource,
"name", target.Name,
)
// Reuse the same canView for items with the same resource
canView, found := canViewFuncs[targetGR]
@@ -192,7 +206,12 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
Resource: target.Resource,
Verb: utils.VerbGetPermissions,
}
r.logger.Debug("compiling list request",
"namespace", item.Namespace,
"group", target.ApiGroup,
"resource", target.Resource,
"verb", utils.VerbGetPermissions,
)
canView, _, err = r.accessClient.Compile(ctx, authInfo, listReq)
if err != nil {
return nil, err
@@ -218,6 +237,13 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
)
continue
}
r.logger.Debug("fetched parent",
"parent", p,
"namespace", item.Namespace,
"group", target.ApiGroup,
"resource", target.Resource,
"name", target.Name,
)
parent = p
}

View File

@@ -246,8 +246,6 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
//nolint:staticcheck // not yet migrated to OpenFeature
enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync)
//nolint:staticcheck // not yet migrated to OpenFeature
enableAuthzApis := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis)
// teams + users must have shorter names because they are often used as part of another name
opts.StorageOptsRegister(iamv0.TeamResourceInfo.GroupResource(), apistore.StorageOptions{
@@ -257,60 +255,6 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
MaximumNameLength: 80,
})
if err := b.UpdateTeamsAPIGroup(opts, storage); err != nil {
return err
}
if err := b.UpdateTeamBindingsAPIGroup(opts, storage, enableZanzanaSync); err != nil {
return err
}
if err := b.UpdateUsersAPIGroup(opts, storage, enableZanzanaSync); err != nil {
return err
}
if err := b.UpdateServiceAccountsAPIGroup(opts, storage); err != nil {
return err
}
// SSO settings apis
if b.ssoLegacyStore != nil {
ssoResource := legacyiamv0.SSOSettingResourceInfo
storage[ssoResource.StoragePath()] = b.ssoLegacyStore
}
if err := b.UpdateExternalGroupMappingAPIGroup(apiGroupInfo, opts, storage); err != nil {
return err
}
if enableAuthzApis {
// v0alpha1
if err := b.UpdateCoreRolesAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil {
return err
}
// Role registration is delegated to the RoleApiInstaller
if err := b.roleApiInstaller.RegisterStorage(apiGroupInfo, &opts, storage); err != nil {
return err
}
if err := b.UpdateRoleBindingsAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil {
return err
}
}
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil {
return err
}
}
apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage
return nil
}
func (b *IdentityAccessManagementAPIBuilder) UpdateTeamsAPIGroup(opts builder.APIGroupOptions, storage map[string]rest.Storage) error {
teamResource := iamv0.TeamResourceInfo
teamUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamResource, opts.OptsGetter)
if err != nil {
@@ -332,10 +276,6 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamsAPIGroup(opts builder.AP
storage[teamResource.StoragePath("groups")] = b.teamGroupsHandler
}
return nil
}
func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts builder.APIGroupOptions, storage map[string]rest.Storage, enableZanzanaSync bool) error {
teamBindingResource := iamv0.TeamBindingResourceInfo
teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter)
if err != nil {
@@ -358,10 +298,8 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui
}
storage[teamBindingResource.StoragePath()] = dw
}
return nil
}
func (b *IdentityAccessManagementAPIBuilder) UpdateUsersAPIGroup(opts builder.APIGroupOptions, storage map[string]rest.Storage, enableZanzanaSync bool) error {
// User store registration
userResource := iamv0.UserResourceInfo
userUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter)
if err != nil {
@@ -387,10 +325,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateUsersAPIGroup(opts builder.AP
storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store)
return nil
}
func (b *IdentityAccessManagementAPIBuilder) UpdateServiceAccountsAPIGroup(opts builder.APIGroupOptions, storage map[string]rest.Storage) error {
// Service Accounts store registration
saResource := iamv0.ServiceAccountResourceInfo
saUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, saResource, opts.OptsGetter)
if err != nil {
@@ -408,10 +343,11 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateServiceAccountsAPIGroup(opts
storage[saResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store)
return nil
}
if b.ssoLegacyStore != nil {
ssoResource := legacyiamv0.SSOSettingResourceInfo
storage[ssoResource.StoragePath()] = b.ssoLegacyStore
}
func (b *IdentityAccessManagementAPIBuilder) UpdateExternalGroupMappingAPIGroup(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions, storage map[string]rest.Storage) error {
extGroupMappingResource := iamv0.ExternalGroupMappingResourceInfo
extGroupMappingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, extGroupMappingResource, opts.OptsGetter)
if err != nil {
@@ -440,47 +376,48 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateExternalGroupMappingAPIGroup(
authzWrapper := storewrapper.New(extGroupMappingStore, iamauthorizer.NewExternalGroupMappingAuthorizer(b.accessClient))
storage[extGroupMappingResource.StoragePath()] = authzWrapper
return nil
}
func (b *IdentityAccessManagementAPIBuilder) UpdateCoreRolesAPIGroup(
apiGroupInfo *genericapiserver.APIGroupInfo,
opts builder.APIGroupOptions,
storage map[string]rest.Storage,
enableZanzanaSync bool,
) error {
coreRoleStore, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage)
if err != nil {
return err
}
if enableZanzanaSync {
b.logger.Info("Enabling hooks for CoreRole to sync to Zanzana")
h := NewRoleHooks(b.zClient, b.zTickets, b.logger)
coreRoleStore.AfterCreate = h.AfterRoleCreate
coreRoleStore.AfterDelete = h.AfterRoleDelete
coreRoleStore.BeginUpdate = h.BeginRoleUpdate
}
storage[iamv0.CoreRoleInfo.StoragePath()] = coreRoleStore
return nil
}
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
// v0alpha1
coreRoleStore, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage)
if err != nil {
return err
}
if enableZanzanaSync {
b.logger.Info("Enabling hooks for CoreRole to sync to Zanzana")
h := NewRoleHooks(b.zClient, b.zTickets, b.logger)
coreRoleStore.AfterCreate = h.AfterRoleCreate
coreRoleStore.AfterDelete = h.AfterRoleDelete
coreRoleStore.BeginUpdate = h.BeginRoleUpdate
}
storage[iamv0.CoreRoleInfo.StoragePath()] = coreRoleStore
func (b *IdentityAccessManagementAPIBuilder) UpdateRoleBindingsAPIGroup(
apiGroupInfo *genericapiserver.APIGroupInfo,
opts builder.APIGroupOptions,
storage map[string]rest.Storage,
enableZanzanaSync bool,
) error {
roleBindingStore, err := NewLocalStore(iamv0.RoleBindingInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.roleBindingsStorage)
if err != nil {
return err
// Role registration is delegated to the RoleApiInstaller
if err := b.roleApiInstaller.RegisterStorage(apiGroupInfo, &opts, storage); err != nil {
return err
}
roleBindingStore, err := NewLocalStore(iamv0.RoleBindingInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.roleBindingsStorage)
if err != nil {
return err
}
if enableZanzanaSync {
b.logger.Info("Enabling hooks for RoleBinding to sync to Zanzana")
roleBindingStore.AfterCreate = b.AfterRoleBindingCreate
roleBindingStore.AfterDelete = b.AfterRoleBindingDelete
roleBindingStore.BeginUpdate = b.BeginRoleBindingUpdate
}
storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore
}
if enableZanzanaSync {
b.logger.Info("Enabling hooks for RoleBinding to sync to Zanzana")
roleBindingStore.AfterCreate = b.AfterRoleBindingCreate
roleBindingStore.AfterDelete = b.AfterRoleBindingDelete
roleBindingStore.BeginUpdate = b.BeginRoleBindingUpdate
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil {
return err
}
}
storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore
apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage
return nil
}

View File

@@ -208,11 +208,6 @@ func (s *preferenceStorage) save(ctx context.Context, obj runtime.Object) (runti
// Create implements rest.Creater.
func (s *preferenceStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
if createValidation != nil {
if err := createValidation(ctx, obj); err != nil {
return nil, err
}
}
return s.save(ctx, obj)
}
@@ -228,12 +223,6 @@ func (s *preferenceStorage) Update(ctx context.Context, name string, objInfo res
return nil, false, err
}
if updateValidation != nil {
if err := updateValidation(ctx, obj, old); err != nil {
return nil, false, err
}
}
obj, err = s.save(ctx, obj)
return obj, false, err
}

View File

@@ -1,14 +1,9 @@
package preferences
import (
"context"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -29,8 +24,7 @@ import (
)
var (
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
_ builder.APIGroupValidation = (*APIBuilder)(nil)
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
)
type APIBuilder struct {
@@ -114,31 +108,3 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
return b.merger.GetAPIRoutes(defs)
}
// Validate validates that the preference object has valid theme and timezone (if specified)
func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
if a.GetResource().Resource != "preferences" {
return nil
}
op := a.GetOperation()
if op != admission.Create && op != admission.Update {
return nil
}
obj := a.GetObject()
p, ok := obj.(*preferences.Preferences)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected Preferences object, got %T", obj))
}
if p.Spec.Timezone != nil && !pref.IsValidTimezone(*p.Spec.Timezone) {
return apierrors.NewBadRequest("invalid timezone: must be a valid IANA timezone (e.g., America/New_York), 'utc', 'browser', or empty string")
}
if p.Spec.Theme != nil && *p.Spec.Theme != "" && !pref.IsValidThemeID(*p.Spec.Theme) {
return apierrors.NewBadRequest("invalid theme")
}
return nil
}

View File

@@ -1,69 +0,0 @@
package provisioning
import (
"context"
"net/http"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
)
type connectionRepositoriesConnector struct{}
func NewConnectionRepositoriesConnector() *connectionRepositoriesConnector {
return &connectionRepositoriesConnector{}
}
func (*connectionRepositoriesConnector) New() runtime.Object {
return &provisioning.ExternalRepositoryList{}
}
func (*connectionRepositoriesConnector) Destroy() {}
func (*connectionRepositoriesConnector) ProducesMIMETypes(verb string) []string {
return []string{"application/json"}
}
func (*connectionRepositoriesConnector) ProducesObject(verb string) any {
return &provisioning.ExternalRepositoryList{}
}
func (*connectionRepositoriesConnector) ConnectMethods() []string {
return []string{http.MethodGet}
}
func (*connectionRepositoriesConnector) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, ""
}
func (c *connectionRepositoriesConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
logger := logging.FromContext(ctx).With("logger", "connection-repositories-connector", "connection_name", name)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
responder.Error(apierrors.NewMethodNotSupported(provisioning.ConnectionResourceInfo.GroupResource(), r.Method))
return
}
logger.Debug("repositories endpoint called but not yet implemented")
// TODO: Implement repository listing from external git provider
// This will require:
// 1. Get the Connection object using logging.Context(r.Context(), logger)
// 2. Use the connection credentials to authenticate with the git provider
// 3. List repositories from the provider (GitHub, GitLab, Bitbucket)
// 4. Return ExternalRepositoryList with Name, Owner, and URL for each repository
responder.Error(apierrors.NewMethodNotSupported(provisioning.ConnectionResourceInfo.GroupResource(), "repositories endpoint not yet implemented"))
}), nil
}
var (
_ rest.Storage = (*connectionRepositoriesConnector)(nil)
_ rest.Connecter = (*connectionRepositoriesConnector)(nil)
_ rest.StorageMetadata = (*connectionRepositoriesConnector)(nil)
)

View File

@@ -1,101 +0,0 @@
package provisioning
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
)
func TestConnectionRepositoriesConnector(t *testing.T) {
connector := NewConnectionRepositoriesConnector()
t.Run("New returns ExternalRepositoryList", func(t *testing.T) {
obj := connector.New()
require.IsType(t, &provisioning.ExternalRepositoryList{}, obj)
})
t.Run("ProducesMIMETypes returns application/json", func(t *testing.T) {
types := connector.ProducesMIMETypes("GET")
require.Equal(t, []string{"application/json"}, types)
})
t.Run("ProducesObject returns ExternalRepositoryList", func(t *testing.T) {
obj := connector.ProducesObject("GET")
require.IsType(t, &provisioning.ExternalRepositoryList{}, obj)
})
t.Run("ConnectMethods returns GET", func(t *testing.T) {
methods := connector.ConnectMethods()
require.Equal(t, []string{http.MethodGet}, methods)
})
t.Run("NewConnectOptions returns no path component", func(t *testing.T) {
obj, hasPath, path := connector.NewConnectOptions()
require.Nil(t, obj)
require.False(t, hasPath)
require.Empty(t, path)
})
t.Run("Connect returns handler that rejects non-GET methods", func(t *testing.T) {
ctx := context.Background()
responder := &mockResponder{}
handler, err := connector.Connect(ctx, "test-connection", nil, responder)
require.NoError(t, err)
require.NotNil(t, handler)
// Test POST method (should be rejected)
req := httptest.NewRequest(http.MethodPost, "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.True(t, responder.called)
require.NotNil(t, responder.err)
require.True(t, apierrors.IsMethodNotSupported(responder.err))
})
t.Run("Connect returns handler that returns not implemented for GET", func(t *testing.T) {
ctx := context.Background()
responder := &mockResponder{}
handler, err := connector.Connect(ctx, "test-connection", nil, responder)
require.NoError(t, err)
require.NotNil(t, handler)
// Test GET method (should return not implemented)
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.True(t, responder.called)
require.NotNil(t, responder.err)
require.True(t, apierrors.IsMethodNotSupported(responder.err))
require.Contains(t, responder.err.Error(), "not yet implemented")
})
}
// mockResponder implements rest.Responder for testing
type mockResponder struct {
called bool
err error
obj runtime.Object
code int
}
func (m *mockResponder) Object(statusCode int, obj runtime.Object) {
m.called = true
m.code = statusCode
m.obj = obj
}
func (m *mockResponder) Error(err error) {
m.called = true
m.err = err
}

View File

@@ -1,254 +0,0 @@
package controller
import (
"context"
"errors"
"fmt"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
informer "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions/provisioning/v0alpha1"
listers "github.com/grafana/grafana/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1"
)
const connectionLoggerName = "provisioning-connection-controller"
const (
connectionMaxAttempts = 3
// connectionHealthyDuration defines how recent a health check must be to be considered "recent" when healthy
connectionHealthyDuration = 5 * time.Minute
// connectionUnhealthyDuration defines how recent a health check must be to be considered "recent" when unhealthy
connectionUnhealthyDuration = 1 * time.Minute
)
type connectionQueueItem struct {
key string
attempts int
}
// ConnectionStatusPatcher defines the interface for updating connection status.
//
//go:generate mockery --name=ConnectionStatusPatcher
type ConnectionStatusPatcher interface {
Patch(ctx context.Context, conn *provisioning.Connection, patchOperations ...map[string]interface{}) error
}
// ConnectionController controls Connection resources.
type ConnectionController struct {
client client.ProvisioningV0alpha1Interface
connLister listers.ConnectionLister
connSynced cache.InformerSynced
logger logging.Logger
statusPatcher ConnectionStatusPatcher
queue workqueue.TypedRateLimitingInterface[*connectionQueueItem]
}
// NewConnectionController creates a new ConnectionController.
func NewConnectionController(
provisioningClient client.ProvisioningV0alpha1Interface,
connInformer informer.ConnectionInformer,
statusPatcher ConnectionStatusPatcher,
) (*ConnectionController, error) {
cc := &ConnectionController{
client: provisioningClient,
connLister: connInformer.Lister(),
connSynced: connInformer.Informer().HasSynced,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[*connectionQueueItem](),
workqueue.TypedRateLimitingQueueConfig[*connectionQueueItem]{
Name: "provisioningConnectionController",
},
),
statusPatcher: statusPatcher,
logger: logging.DefaultLogger.With("logger", connectionLoggerName),
}
_, err := connInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: cc.enqueue,
UpdateFunc: func(oldObj, newObj interface{}) {
cc.enqueue(newObj)
},
})
if err != nil {
return nil, err
}
return cc, nil
}
func (cc *ConnectionController) enqueue(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
cc.logger.Error("failed to get key for object", "error", err)
return
}
cc.queue.Add(&connectionQueueItem{key: key})
}
// Run starts the ConnectionController.
func (cc *ConnectionController) Run(ctx context.Context, workerCount int) {
defer utilruntime.HandleCrash()
defer cc.queue.ShutDown()
cc.logger.Info("starting connection controller", "workers", workerCount)
for i := 0; i < workerCount; i++ {
go wait.UntilWithContext(ctx, cc.runWorker, time.Second)
}
<-ctx.Done()
cc.logger.Info("shutting down connection controller")
}
func (cc *ConnectionController) runWorker(ctx context.Context) {
for cc.processNextWorkItem(ctx) {
}
}
func (cc *ConnectionController) processNextWorkItem(ctx context.Context) bool {
item, quit := cc.queue.Get()
if quit {
return false
}
defer cc.queue.Done(item)
logger := logging.FromContext(ctx).With("work_key", item.key)
logger.Info("ConnectionController processing key")
err := cc.process(ctx, item)
if err == nil {
cc.queue.Forget(item)
return true
}
item.attempts++
logger = logger.With("error", err, "attempts", item.attempts)
logger.Error("ConnectionController failed to process key")
if item.attempts >= connectionMaxAttempts {
logger.Error("ConnectionController failed too many times")
cc.queue.Forget(item)
return true
}
if !apierrors.IsServiceUnavailable(err) {
logger.Info("ConnectionController will not retry")
cc.queue.Forget(item)
return true
}
logger.Info("ConnectionController will retry as service is unavailable")
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", item, err))
cc.queue.AddRateLimited(item)
return true
}
func (cc *ConnectionController) process(ctx context.Context, item *connectionQueueItem) error {
logger := cc.logger.With("key", item.key)
ctx = logging.Context(ctx, logger)
namespace, name, err := cache.SplitMetaNamespaceKey(item.key)
if err != nil {
return err
}
conn, err := cc.connLister.Connections(namespace).Get(name)
switch {
case apierrors.IsNotFound(err):
return errors.New("connection not found in cache")
case err != nil:
return err
}
// Skip if being deleted
if conn.DeletionTimestamp != nil {
logger.Info("connection is being deleted, skipping")
return nil
}
hasSpecChanged := conn.Generation != conn.Status.ObservedGeneration
shouldCheckHealth := cc.shouldCheckHealth(conn)
// Determine the main triggering condition
switch {
case hasSpecChanged:
logger.Info("spec changed, reconciling", "generation", conn.Generation, "observedGeneration", conn.Status.ObservedGeneration)
case shouldCheckHealth:
logger.Info("health is stale, refreshing", "lastChecked", conn.Status.Health.Checked, "healthy", conn.Status.Health.Healthy)
default:
logger.Debug("skipping as conditions are not met", "generation", conn.Generation, "observedGeneration", conn.Status.ObservedGeneration)
return nil
}
// For now, just update the state to connected, health to healthy, and observed generation
// Future: Add credential validation logic here
patchOperations := []map[string]interface{}{}
// Only update observedGeneration when spec changes
if hasSpecChanged {
patchOperations = append(patchOperations, map[string]interface{}{
"op": "replace",
"path": "/status/observedGeneration",
"value": conn.Generation,
})
}
// Always update state and health
patchOperations = append(patchOperations,
map[string]interface{}{
"op": "replace",
"path": "/status/state",
"value": provisioning.ConnectionStateConnected,
},
map[string]interface{}{
"op": "replace",
"path": "/status/health",
"value": provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().UnixMilli(),
},
},
)
if err := cc.statusPatcher.Patch(ctx, conn, patchOperations...); err != nil {
return fmt.Errorf("failed to update connection status: %w", err)
}
logger.Info("connection reconciled successfully")
return nil
}
// shouldCheckHealth determines if a connection health check should be performed.
func (cc *ConnectionController) shouldCheckHealth(conn *provisioning.Connection) bool {
// If the connection has been updated, always check health
if conn.Generation != conn.Status.ObservedGeneration {
return true
}
// Check if health check is stale
return !cc.hasRecentHealthCheck(conn.Status.Health)
}
// hasRecentHealthCheck checks if a health check was performed recently.
func (cc *ConnectionController) hasRecentHealthCheck(healthStatus provisioning.HealthStatus) bool {
if healthStatus.Checked == 0 {
return false // Never checked
}
age := time.Since(time.UnixMilli(healthStatus.Checked))
if healthStatus.Healthy {
return age <= connectionHealthyDuration
}
return age <= connectionUnhealthyDuration
}

View File

@@ -1,287 +0,0 @@
package controller
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
)
func TestConnectionController_shouldCheckHealth(t *testing.T) {
testCases := []struct {
name string
conn *provisioning.Connection
expected bool
}{
{
name: "should check health when generation differs from observed",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Generation: 2,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
},
},
expected: true,
},
{
name: "should check health when never checked before",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Checked: 0,
},
},
},
expected: true,
},
{
name: "should check health when healthy check is stale (>5 min)",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().Add(-6 * time.Minute).UnixMilli(),
},
},
},
expected: true,
},
{
name: "should check health when unhealthy check is stale (>1 min)",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: false,
Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
},
},
},
expected: true,
},
{
name: "should not check health when healthy check is recent (<5 min)",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
},
},
},
expected: false,
},
{
name: "should not check health when unhealthy check is recent (<1 min)",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: false,
Checked: time.Now().Add(-30 * time.Second).UnixMilli(),
},
},
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cc := &ConnectionController{}
result := cc.shouldCheckHealth(tc.conn)
assert.Equal(t, tc.expected, result)
})
}
}
func TestConnectionController_hasRecentHealthCheck(t *testing.T) {
testCases := []struct {
name string
healthStatus provisioning.HealthStatus
expected bool
}{
{
name: "never checked",
healthStatus: provisioning.HealthStatus{
Checked: 0,
},
expected: false,
},
{
name: "healthy and recent",
healthStatus: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
},
expected: true,
},
{
name: "healthy and stale",
healthStatus: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().Add(-10 * time.Minute).UnixMilli(),
},
expected: false,
},
{
name: "unhealthy and recent",
healthStatus: provisioning.HealthStatus{
Healthy: false,
Checked: time.Now().Add(-30 * time.Second).UnixMilli(),
},
expected: true,
},
{
name: "unhealthy and stale",
healthStatus: provisioning.HealthStatus{
Healthy: false,
Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cc := &ConnectionController{}
result := cc.hasRecentHealthCheck(tc.healthStatus)
assert.Equal(t, tc.expected, result)
})
}
}
func TestConnectionController_reconcileConditions(t *testing.T) {
testCases := []struct {
name string
conn *provisioning.Connection
expectReconcile bool
expectSpecChanged bool
description string
}{
{
name: "skip when being deleted",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Name: "test-conn",
Namespace: "default",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
},
},
expectReconcile: false,
expectSpecChanged: false,
description: "deleted connections should be skipped",
},
{
name: "skip when no changes needed",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Name: "test-conn",
Namespace: "default",
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().UnixMilli(),
},
},
},
expectReconcile: false,
expectSpecChanged: false,
description: "no reconcile when generation matches and health is recent",
},
{
name: "reconcile when spec changed",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Name: "test-conn",
Namespace: "default",
Generation: 2,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().UnixMilli(),
},
},
},
expectReconcile: true,
expectSpecChanged: true,
description: "reconcile when generation differs",
},
{
name: "reconcile when health is stale",
conn: &provisioning.Connection{
ObjectMeta: metav1.ObjectMeta{
Name: "test-conn",
Namespace: "default",
Generation: 1,
},
Status: provisioning.ConnectionStatus{
ObservedGeneration: 1,
Health: provisioning.HealthStatus{
Healthy: true,
Checked: time.Now().Add(-10 * time.Minute).UnixMilli(),
},
},
},
expectReconcile: true,
expectSpecChanged: false,
description: "reconcile when health check is stale",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cc := &ConnectionController{}
// Test the core reconciliation conditions
if tc.conn.DeletionTimestamp != nil {
assert.False(t, tc.expectReconcile, tc.description)
return
}
hasSpecChanged := tc.conn.Generation != tc.conn.Status.ObservedGeneration
shouldCheckHealth := cc.shouldCheckHealth(tc.conn)
needsReconcile := hasSpecChanged || shouldCheckHealth
assert.Equal(t, tc.expectReconcile, needsReconcile, tc.description)
assert.Equal(t, tc.expectSpecChanged, hasSpecChanged, "spec changed check")
})
}
}
func TestConnectionController_processNextWorkItem(t *testing.T) {
t.Run("returns false when queue is shut down", func(t *testing.T) {
cc := &ConnectionController{}
// This test verifies the structure is correct
assert.NotNil(t, cc)
})
}

View File

@@ -480,16 +480,6 @@ func (b *APIBuilder) authorizeConnectionSubresource(ctx context.Context, a autho
Namespace: a.GetNamespace(),
}, ""))
// Repositories is read-only
case "repositories":
return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{
Verb: apiutils.VerbGet,
Group: provisioning.GROUP,
Resource: provisioning.ConnectionResourceInfo.GetName(),
Name: a.GetName(),
Namespace: a.GetNamespace(),
}, ""))
default:
id, err := identity.GetRequester(ctx)
if err != nil {
@@ -613,7 +603,6 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
storage[provisioning.ConnectionResourceInfo.StoragePath()] = connectionsStore
storage[provisioning.ConnectionResourceInfo.StoragePath("status")] = connectionStatusStorage
storage[provisioning.ConnectionResourceInfo.StoragePath("repositories")] = NewConnectionRepositoriesConnector()
// TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place
storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories))
@@ -817,10 +806,8 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
sharedInformerFactory := informers.NewSharedInformerFactory(c, 60*time.Second)
repoInformer := sharedInformerFactory.Provisioning().V0alpha1().Repositories()
jobInformer := sharedInformerFactory.Provisioning().V0alpha1().Jobs()
connInformer := sharedInformerFactory.Provisioning().V0alpha1().Connections()
go repoInformer.Informer().Run(postStartHookCtx.Done())
go jobInformer.Informer().Run(postStartHookCtx.Done())
go connInformer.Informer().Run(postStartHookCtx.Done())
// Create the repository resources factory
repositoryListerWrapper := func(ctx context.Context) ([]provisioning.Repository, error) {
@@ -941,18 +928,6 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
go repoController.Run(postStartHookCtx.Context, repoControllerWorkers)
// Create and run connection controller
connStatusPatcher := appcontroller.NewConnectionStatusPatcher(b.GetClient())
connController, err := controller.NewConnectionController(
b.GetClient(),
connInformer,
connStatusPatcher,
)
if err != nil {
return err
}
go connController.Run(postStartHookCtx.Context, repoControllerWorkers)
// If Loki not used, initialize the API client-based history writer and start the controller for history jobs
if b.jobHistoryLoki == nil {
// Create HistoryJobController for cleanup of old job history entries
@@ -1272,23 +1247,6 @@ spec:
oas.Paths.Paths[repoprefix+"/jobs/{uid}"] = sub
}
// Document connection repositories endpoint
connectionprefix := root + "namespaces/{namespace}/connections/{name}"
sub = oas.Paths.Paths[connectionprefix+"/repositories"]
if sub != nil {
sub.Get.Description = "List repositories available from the external git provider through this connection"
sub.Get.Summary = "List external repositories"
sub.Get.Parameters = []*spec3.Parameter{}
sub.Post = nil
sub.Put = nil
sub.Delete = nil
// Replace the content type for this response
mt := sub.Get.Responses.StatusCodeResponses[200].Content
s := defs[defsBase+"ExternalRepositoryList"].Schema
mt["*/*"].Schema = &s
}
// Run all extra post-processors.
for _, extra := range b.extras {
if err := extra.PostProcessOpenAPI(oas); err != nil {

View File

@@ -4,8 +4,6 @@ import (
"context"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
@@ -437,11 +435,6 @@ func anonymousRoleBindingsCollector(cfg *setting.Cfg, store db.DB) legacyTupleCo
func zanzanaCollector(relations []string) zanzanaTupleCollector {
return func(ctx context.Context, client zanzana.Client, object string, namespace string) (map[string]*openfgav1.TupleKey, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.dualwrite.resourceReconciler.zanzanaTupleCollector",
trace.WithAttributes(attribute.String("namespace", namespace)),
)
defer span.End()
// list will use continuation token to collect all tuples for object and relation
list := func(relation string) ([]*openfgav1.Tuple, error) {
first, err := client.Read(ctx, &authzextv1.ReadRequest{

View File

@@ -6,8 +6,6 @@ import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
claims "github.com/grafana/authlib/types"
@@ -50,12 +48,6 @@ func newResourceReconciler(name string, legacy legacyTupleCollector, zanzanaColl
}
func (r resourceReconciler) reconcile(ctx context.Context, namespace string) error {
ctx, span := tracer.Start(ctx, "accesscontrol.dualwrite.resourceReconciler.reconcile",
trace.WithAttributes(attribute.String("namespace", namespace)),
trace.WithAttributes(attribute.String("reconciler", r.name)),
)
defer span.End()
info, err := claims.ParseNamespace(namespace)
if err != nil {
return err
@@ -71,12 +63,7 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err
}
// 1. Fetch grafana resources stored in grafana db.
legacyCtx, legacySpan := tracer.Start(ctx, "accesscontrol.dualwrite.resourceReconciler.legacyCollector",
trace.WithAttributes(attribute.String("namespace", namespace)),
trace.WithAttributes(attribute.String("reconciler", r.name)),
)
res, err := r.legacy(legacyCtx, info.OrgID)
legacySpan.End()
res, err := r.legacy(ctx, info.OrgID)
if err != nil {
return fmt.Errorf("failed to collect legacy tuples for %s: %w", r.name, err)
}
@@ -224,12 +211,6 @@ func (r resourceReconciler) collectOrphanDeletes(
}
func (r resourceReconciler) readAllTuples(ctx context.Context, namespace string) ([]*authzextv1.Tuple, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.dualwrite.resourceReconciler.zanzana.readAllTuples",
trace.WithAttributes(attribute.String("namespace", namespace)),
trace.WithAttributes(attribute.String("reconciler", r.name)),
)
defer span.End()
var (
out []*authzextv1.Tuple
continueToken string

View File

@@ -542,9 +542,6 @@ func (d *dashboardStore) saveDashboard(ctx context.Context, sess *db.Session, cm
tags := dash.GetTags()
if len(tags) > 0 {
for _, tag := range tags {
if len(tag) > 50 {
return nil, dashboards.ErrDashboardTagTooLong
}
if _, err := sess.Insert(dashboardTag{DashboardId: dash.ID, Term: tag, OrgID: dash.OrgID, DashboardUID: dash.UID}); err != nil {
return nil, err
}

View File

@@ -79,11 +79,6 @@ var (
Reason: "message too long, max 500 characters",
StatusCode: 400,
}
ErrDashboardTagTooLong = dashboardaccess.DashboardErr{
Reason: "dashboard tag too long, max 50 characters",
StatusCode: 400,
Status: "tag-too-long",
}
ErrDashboardCannotSaveProvisionedDashboard = dashboardaccess.DashboardErr{
Reason: "Cannot save provisioned dashboard",
StatusCode: 400,

View File

@@ -20,10 +20,6 @@ func UpdatePreferencesFor(ctx context.Context,
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
}
if !pref.IsValidTimezone(dtoCmd.Timezone) {
return response.Error(http.StatusBadRequest, "Invalid timezone. Must be a valid IANA timezone (e.g., America/New_York), 'utc', 'browser', or empty string", nil)
}
// convert dashboard UID to ID in order to store internally if it exists in the query, otherwise take the id from query
// nolint:staticcheck
dashboardID := dtoCmd.HomeDashboardID

View File

@@ -1,21 +0,0 @@
package pref
import (
"time"
)
// IsValidTimezone checks if the timezone string is valid.
// It accepts:
// - "" - uses default
// - "utc"
// - "browser"
// - Any valid IANA timezone (e.g., "America/New_York", "Europe/London")
func IsValidTimezone(timezone string) bool {
if timezone == "" || timezone == "utc" || timezone == "browser" {
return true
}
// try to load as IANA timezone
_, err := time.LoadLocation(timezone)
return err == nil
}

View File

@@ -1,38 +0,0 @@
package pref
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValidTimezone(t *testing.T) {
tests := []struct {
timezone string
valid bool
}{
{
timezone: "utc",
valid: true,
},
{
timezone: "browser",
valid: true,
},
{
timezone: "Europe/London",
valid: true,
},
{
timezone: "invalid",
valid: false,
},
{
timezone: "",
valid: true,
},
}
for _, test := range tests {
assert.Equal(t, test.valid, IsValidTimezone(test.timezone))
}
}

View File

@@ -36,8 +36,6 @@ type SecretsManagerSettings struct {
// How long to wait for the process to clean up a secure value to complete.
GCWorkerPerSecureValueCleanupTimeout time.Duration
// Whether to register the MT CRUD API
RegisterAPIServer bool
// Whether to create the MT secrets management database
RunSecretsDBMigrations bool
// Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true.
@@ -66,7 +64,6 @@ func (cfg *Cfg) readSecretsManagerSettings() {
cfg.SecretsManagement.GCWorkerPollInterval = secretsMgmt.Key("gc_worker_poll_interval").MustDuration(1 * time.Minute)
cfg.SecretsManagement.GCWorkerPerSecureValueCleanupTimeout = secretsMgmt.Key("gc_worker_per_request_timeout").MustDuration(5 * time.Second)
cfg.SecretsManagement.RegisterAPIServer = secretsMgmt.Key("register_api_server").MustBool(true)
cfg.SecretsManagement.RunSecretsDBMigrations = secretsMgmt.Key("run_secrets_db_migrations").MustBool(true)
cfg.SecretsManagement.RunDataKeyMigration = secretsMgmt.Key("run_data_key_migration").MustBool(true)

View File

@@ -171,28 +171,6 @@ domain = example.com
assert.Empty(t, cfg.SecretsManagement.ConfiguredKMSProviders)
})
t.Run("should handle configuration with register_api_server disabled", func(t *testing.T) {
iniContent := `
[secrets_manager]
register_api_server = false
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.False(t, cfg.SecretsManagement.RegisterAPIServer)
})
t.Run("should handle configuration without register_api_server set", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = aws_kms
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.True(t, cfg.SecretsManagement.RegisterAPIServer)
})
t.Run("should handle configuration with run_secrets_db_migrations disabled", func(t *testing.T) {
iniContent := `
[secrets_manager]

View File

@@ -864,15 +864,11 @@ func (d *dataStore) applyBackwardsCompatibleChanges(ctx context.Context, tx db.T
return nil
}
generation := event.Object.GetGeneration()
if key.Action == DataActionDeleted {
generation = 0
}
_, err := dbutil.Exec(ctx, tx, sqlKVUpdateLegacyResourceHistory, sqlKVLegacyUpdateHistoryRequest{
SQLTemplate: sqltemplate.New(kv.dialect),
GUID: key.GUID,
PreviousRV: event.PreviousRV,
Generation: generation,
Generation: event.Object.GetGeneration(),
})
if err != nil {
@@ -914,7 +910,6 @@ func (d *dataStore) applyBackwardsCompatibleChanges(ctx context.Context, tx db.T
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
Action: action,
Folder: key.Folder,
PreviousRV: event.PreviousRV,
})
@@ -925,7 +920,6 @@ func (d *dataStore) applyBackwardsCompatibleChanges(ctx context.Context, tx db.T
case DataActionDeleted:
_, err := dbutil.Exec(ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{
SQLTemplate: sqltemplate.New(kv.dialect),
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,

View File

@@ -28,7 +28,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager"
"github.com/grafana/grafana/pkg/util/scheduler"
)
@@ -816,7 +815,7 @@ func (s *server) update(ctx context.Context, user claims.AuthInfo, req *resource
// TODO: once we know the client is always sending the RV, require ResourceVersion > 0
// See: https://github.com/grafana/grafana/pull/111866
if req.ResourceVersion > 0 && !rvmanager.IsRvEqual(latest.ResourceVersion, req.ResourceVersion) {
if req.ResourceVersion > 0 && latest.ResourceVersion != req.ResourceVersion {
return &resourcepb.UpdateResponse{
Error: &ErrOptimisticLockingFailed,
}, nil
@@ -884,7 +883,7 @@ func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resource
rsp.Error = latest.Error
return rsp, nil
}
if req.ResourceVersion > 0 && !rvmanager.IsRvEqual(latest.ResourceVersion, req.ResourceVersion) {
if req.ResourceVersion > 0 && latest.ResourceVersion != req.ResourceVersion {
rsp.Error = &ErrOptimisticLockingFailed
return rsp, nil
}

View File

@@ -107,20 +107,16 @@ func TestIntegrationSQLStorageAndSQLKVCompatibilityTests(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
t.Cleanup(db.CleanupTestDB)
newKvBackend := func(ctx context.Context) (resource.StorageBackend, sqldb.DB) {
return unitest.NewTestSqlKvBackend(t, ctx, true)
}
t.Run("IsHA (polling notifier)", func(t *testing.T) {
unitest.RunSQLStorageBackendCompatibilityTest(t, func(ctx context.Context) (resource.StorageBackend, sqldb.DB) {
return newTestBackend(t, true, 0)
}, newKvBackend, nil)
}, nil)
})
t.Run("NotHA (in process notifier)", func(t *testing.T) {
unitest.RunSQLStorageBackendCompatibilityTest(t, func(ctx context.Context) (resource.StorageBackend, sqldb.DB) {
return newTestBackend(t, false, 0)
}, newKvBackend, nil)
}, nil)
})
}

View File

@@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/bwmarrin/snowflake"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
@@ -43,6 +44,7 @@ const (
TestCreateNewResource = "create new resource"
TestGetResourceLastImportTime = "get resource last import time"
TestOptimisticLocking = "optimistic locking on concurrent writes"
TestKeyPathGeneration = "key_path generation"
)
type NewBackendFunc func(ctx context.Context) resource.StorageBackend
@@ -104,6 +106,37 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp
}
}
func RunSQLStorageBackendCompatibilityTest(t *testing.T, newBackend NewBackendWithDBFunc, opts *TestOptions) {
if opts == nil {
opts = &TestOptions{}
}
if opts.NSPrefix == "" {
opts.NSPrefix = GenerateRandomNSPrefix()
}
t.Logf("Running tests with namespace prefix: %s", opts.NSPrefix)
cases := []struct {
name string
fn func(*testing.T, resource.StorageBackend, string, sqldb.DB)
}{
{TestKeyPathGeneration, runTestIntegrationBackendKeyPathGeneration},
}
for _, tc := range cases {
if shouldSkip := opts.SkipTests[tc.name]; shouldSkip {
t.Logf("Skipping test: %s", tc.name)
continue
}
t.Run(tc.name, func(t *testing.T) {
backend, db := newBackend(context.Background())
tc.fn(t, backend, opts.NSPrefix, db)
})
}
}
func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
Claims: jwt.Claims{
@@ -1726,3 +1759,222 @@ func runTestIntegrationBackendOptimisticLocking(t *testing.T, backend resource.S
require.LessOrEqual(t, successes, 1, "at most one create should succeed (errors: %v)", errorMessages)
})
}
func runTestIntegrationBackendKeyPathGeneration(t *testing.T, backend resource.StorageBackend, nsPrefix string, db sqldb.DB) {
ctx := testutil.NewDefaultTestContext(t)
t.Run("Create resource", func(t *testing.T) {
// Create a test resource
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: nsPrefix + "-default",
Name: "test-playlist-crud",
}
// Create the K8s unstructured object
testObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "playlist.grafana.app/v0alpha1",
"kind": "Playlist",
"metadata": map[string]interface{}{
"name": "test-playlist-crud",
"namespace": nsPrefix + "-default",
"uid": "test-uid-crud-123",
},
"spec": map[string]interface{}{
"title": "My Test Playlist",
},
},
}
// Get metadata accessor
metaAccessor, err := utils.MetaAccessor(testObj)
require.NoError(t, err)
// Serialize to JSON
jsonBytes, err := testObj.MarshalJSON()
require.NoError(t, err)
// Create WriteEvent
writeEvent := resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: key,
Value: jsonBytes,
Object: metaAccessor,
PreviousRV: 0, // Always 0 for new resources
GUID: "create-guid-crud-123",
}
// Create the resource using WriteEvent
createRV, err := backend.WriteEvent(ctx, writeEvent)
require.NoError(t, err)
require.Greater(t, createRV, int64(0))
// Verify created resource key_path
verifyKeyPath(t, db, ctx, key, "created", createRV, "")
t.Run("Update resource", func(t *testing.T) {
// Update the resource
testObj.Object["spec"] = map[string]interface{}{
"title": "My Updated Playlist",
}
updatedMetaAccessor, err := utils.MetaAccessor(testObj)
require.NoError(t, err)
updatedJsonBytes, err := testObj.MarshalJSON()
require.NoError(t, err)
updateEvent := resource.WriteEvent{
Type: resourcepb.WatchEvent_MODIFIED,
Key: key,
Value: updatedJsonBytes,
Object: updatedMetaAccessor,
PreviousRV: createRV,
GUID: fmt.Sprintf("update-guid-%d", createRV),
}
// Update the resource
updateRV, err := backend.WriteEvent(ctx, updateEvent)
require.NoError(t, err)
require.Greater(t, updateRV, createRV)
// Verify updated resource key_path
verifyKeyPath(t, db, ctx, key, "updated", updateRV, "")
t.Run("Delete resource", func(t *testing.T) {
deleteEvent := resource.WriteEvent{
Type: resourcepb.WatchEvent_DELETED,
Key: key,
Value: updatedJsonBytes, // Keep the last known value
Object: updatedMetaAccessor,
PreviousRV: updateRV,
GUID: fmt.Sprintf("delete-guid-%d", updateRV),
}
// Delete the resource
deleteRV, err := backend.WriteEvent(ctx, deleteEvent)
require.NoError(t, err)
require.Greater(t, deleteRV, updateRV)
// Verify deleted resource key_path
verifyKeyPath(t, db, ctx, key, "deleted", deleteRV, "")
})
})
})
t.Run("Resource with folder", func(t *testing.T) {
// Create a resource in a folder
folderKey := &resourcepb.ResourceKey{
Group: "dashboard.grafana.app",
Resource: "dashboards",
Namespace: nsPrefix + "-default",
Name: "my-dashboard",
}
// Create dashboard object with folder
dashboardObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "dashboard.grafana.app/v0alpha1",
"kind": "Dashboard",
"metadata": map[string]interface{}{
"name": "my-dashboard",
"namespace": nsPrefix + "-default",
"uid": "dash-uid-456",
"annotations": map[string]interface{}{
"grafana.app/folder": "test-folder",
},
},
"spec": map[string]interface{}{
"title": "My Dashboard",
},
},
}
folderMetaAccessor, err := utils.MetaAccessor(dashboardObj)
require.NoError(t, err)
folderJsonBytes, err := dashboardObj.MarshalJSON()
require.NoError(t, err)
folderWriteEvent := resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: folderKey,
Value: folderJsonBytes,
Object: folderMetaAccessor,
PreviousRV: 0,
GUID: "folder-guid-456",
}
// Create the dashboard in folder
folderRV, err := backend.WriteEvent(ctx, folderWriteEvent)
require.NoError(t, err)
require.Greater(t, folderRV, int64(0))
// Verify folder resource key_path includes folder
verifyKeyPath(t, db, ctx, folderKey, "created", folderRV, "test-folder")
})
}
// verifyKeyPath is a helper function to verify key_path generation
func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resourcepb.ResourceKey, action string, resourceVersion int64, expectedFolder string) {
var query string
if db.DriverName() == "postgres" {
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = $1 AND name = $2 AND resource_version = $3"
} else {
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = ? AND name = ? AND resource_version = ?"
}
rows, err := db.QueryContext(ctx, query, key.Namespace, key.Name, resourceVersion)
require.NoError(t, err)
require.True(t, rows.Next())
var keyPath string
var actualRV int64
var actualAction int
var actualFolder string
err = rows.Scan(&keyPath, &actualRV, &actualAction, &actualFolder)
require.NoError(t, err)
err = rows.Close()
require.NoError(t, err)
// Verify basic key_path format
require.Contains(t, keyPath, "unified/data/")
require.Contains(t, keyPath, key.Group)
require.Contains(t, keyPath, key.Resource)
require.Contains(t, keyPath, key.Namespace)
require.Contains(t, keyPath, key.Name)
// Verify action suffix
require.Contains(t, keyPath, fmt.Sprintf("~%s~", action))
// Verify snowflake calculation
expectedSnowflake := (((resourceVersion / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (resourceVersion % 1000)
require.Contains(t, keyPath, fmt.Sprintf("/%d~", expectedSnowflake), fmt.Sprintf("actual RV: %d", actualRV))
// Verify folder if specified
if expectedFolder != "" {
require.Equal(t, expectedFolder, actualFolder)
require.Contains(t, keyPath, expectedFolder)
}
// Verify action code matches
var expectedActionCode int
switch action {
case "created":
expectedActionCode = 1
case "updated":
expectedActionCode = 2
case "deleted":
expectedActionCode = 3
}
require.Equal(t, expectedActionCode, actualAction)
t.Logf("Action: %s, RV: %d, Snowflake: %d", action, resourceVersion, expectedSnowflake)
t.Logf("Key_path: %s", keyPath)
if expectedFolder != "" {
t.Logf("Folder: %s", actualFolder)
}
}

View File

@@ -1,716 +0,0 @@
package test
import (
"context"
"fmt"
"strings"
"testing"
"github.com/bwmarrin/snowflake"
"github.com/stretchr/testify/require"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/storage/unified/sql/rvmanager"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/grafana/grafana/pkg/util/testutil"
)
func NewTestSqlKvBackend(t *testing.T, ctx context.Context, withRvManager bool) (resource.KVBackend, sqldb.DB) {
dbstore := db.InitTestDB(t)
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
require.NoError(t, err)
kv, err := resource.NewSQLKV(eDB)
require.NoError(t, err)
db, err := eDB.Init(ctx)
require.NoError(t, err)
kvOpts := resource.KVBackendOptions{
KvStore: kv,
}
if withRvManager {
dialect := sqltemplate.DialectForDriver(db.DriverName())
rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{
Dialect: dialect,
DB: db,
})
require.NoError(t, err)
kvOpts.RvManager = rvManager
}
backend, err := resource.NewKVStorageBackend(kvOpts)
require.NoError(t, err)
return backend, db
}
func RunSQLStorageBackendCompatibilityTest(t *testing.T, newSqlBackend, newKvBackend NewBackendWithDBFunc, opts *TestOptions) {
if opts == nil {
opts = &TestOptions{}
}
if opts.NSPrefix == "" {
opts.NSPrefix = GenerateRandomNSPrefix()
}
t.Logf("Running tests with namespace prefix: %s", opts.NSPrefix)
cases := []struct {
name string
fn func(*testing.T, resource.StorageBackend, resource.StorageBackend, string, sqldb.DB)
}{
{"key_path generation", runTestIntegrationBackendKeyPathGeneration},
{"sql backend fields compatibility", runTestSQLBackendFieldsCompatibility},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if opts.SkipTests[tc.name] {
t.Skip()
}
kvbackend, db := newKvBackend(t.Context())
sqlbackend, _ := newSqlBackend(t.Context())
tc.fn(t, sqlbackend, kvbackend, opts.NSPrefix, db)
})
}
}
func runTestIntegrationBackendKeyPathGeneration(t *testing.T, sqlBackend, kvBackend resource.StorageBackend, nsPrefix string, db sqldb.DB) {
ctx := testutil.NewDefaultTestContext(t)
// Test SQL backend with 3 writes, 3 updates, 3 deletes
t.Run("SQL Backend Operations", func(t *testing.T) {
runKeyPathTest(t, sqlBackend, nsPrefix+"-sql", db, ctx)
})
// Test SQL KV backend with 3 writes, 3 updates, 3 deletes
t.Run("SQL KV Backend Operations", func(t *testing.T) {
runKeyPathTest(t, kvBackend, nsPrefix+"-kv", db, ctx)
})
}
// runKeyPathTest performs 3 writes, 3 updates, and 3 deletes on a backend then verifies that key_path is properly
// generated across both backends
func runKeyPathTest(t *testing.T, backend resource.StorageBackend, nsPrefix string, db sqldb.DB, ctx context.Context) {
// Create storage server from backend
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
Backend: backend,
AccessClient: claims.FixedAccessClient(true), // Allow all operations for testing
})
require.NoError(t, err)
// Track the current resource version for each resource (index 0, 1, 2 for resources 1, 2, 3)
currentRVs := make([]int64, 3)
// Create 3 resources
for i := 1; i <= 3; i++ {
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: nsPrefix,
Name: fmt.Sprintf("test-playlist-%d", i),
}
// Create resource JSON with folder annotation for resource 2
resourceJSON := fmt.Sprintf(`{
"apiVersion": "playlist.grafana.app/v0alpha1",
"kind": "Playlist",
"metadata": {
"name": "test-playlist-%d",
"namespace": "%s",
"uid": "test-uid-%d"%s
},
"spec": {
"title": "My Test Playlist %d"
}
}`, i, nsPrefix, i, getAnnotationsJSON(i == 2), i)
// Create the resource using server.Create
created, err := server.Create(ctx, &resourcepb.CreateRequest{
Key: key,
Value: []byte(resourceJSON),
})
require.NoError(t, err)
require.Nil(t, created.Error)
require.Greater(t, created.ResourceVersion, int64(0))
currentRVs[i-1] = created.ResourceVersion
// Verify created resource key_path (with folder for resource 2)
if i == 2 {
verifyKeyPath(t, db, ctx, key, "created", created.ResourceVersion, "test-folder")
} else {
verifyKeyPath(t, db, ctx, key, "created", created.ResourceVersion, "")
}
}
// Update the 3 resources
for i := 1; i <= 3; i++ {
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: nsPrefix,
Name: fmt.Sprintf("test-playlist-%d", i),
}
// Create updated resource JSON with folder annotation for resource 2
updatedResourceJSON := fmt.Sprintf(`{
"apiVersion": "playlist.grafana.app/v0alpha1",
"kind": "Playlist",
"metadata": {
"name": "test-playlist-%d",
"namespace": "%s",
"uid": "test-uid-%d"%s
},
"spec": {
"title": "My Updated Playlist %d"
}
}`, i, nsPrefix, i, getAnnotationsJSON(i == 2), i)
// Update the resource using server.Update
updated, err := server.Update(ctx, &resourcepb.UpdateRequest{
Key: key,
Value: []byte(updatedResourceJSON),
ResourceVersion: currentRVs[i-1], // Use the resource version returned by previous operation
})
require.NoError(t, err)
require.Nil(t, updated.Error)
require.Greater(t, updated.ResourceVersion, currentRVs[i-1])
currentRVs[i-1] = updated.ResourceVersion // Update to the latest resource version
// Verify updated resource key_path (with folder for resource 2)
if i == 2 {
verifyKeyPath(t, db, ctx, key, "updated", updated.ResourceVersion, "test-folder")
} else {
verifyKeyPath(t, db, ctx, key, "updated", updated.ResourceVersion, "")
}
}
// Delete the 3 resources
for i := 1; i <= 3; i++ {
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: nsPrefix,
Name: fmt.Sprintf("test-playlist-%d", i),
}
// Delete the resource using server.Delete
deleted, err := server.Delete(ctx, &resourcepb.DeleteRequest{
Key: key,
ResourceVersion: currentRVs[i-1], // Use the resource version from previous operation
})
require.NoError(t, err)
require.Greater(t, deleted.ResourceVersion, currentRVs[i-1])
// Verify deleted resource key_path (with folder for resource 2)
if i == 2 {
verifyKeyPath(t, db, ctx, key, "deleted", deleted.ResourceVersion, "test-folder")
} else {
verifyKeyPath(t, db, ctx, key, "deleted", deleted.ResourceVersion, "")
}
}
}
// verifyKeyPath is a helper function to verify key_path generation
func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resourcepb.ResourceKey, action string, resourceVersion int64, expectedFolder string) {
var query string
if db.DriverName() == "postgres" {
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = $1 AND name = $2 AND resource_version = $3"
} else {
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = ? AND name = ? AND resource_version = ?"
}
rows, err := db.QueryContext(ctx, query, key.Namespace, key.Name, resourceVersion)
require.NoError(t, err)
require.True(t, rows.Next(), "Resource not found in resource_history table - both SQL and KV backends should write to this table")
var keyPath string
var actualRV int64
var actualAction int
var actualFolder string
err = rows.Scan(&keyPath, &actualRV, &actualAction, &actualFolder)
require.NoError(t, err)
// Ensure there's exactly one row and no errors
require.False(t, rows.Next())
require.NoError(t, rows.Err())
// Verify basic key_path format
require.Contains(t, keyPath, "unified/data/")
require.Contains(t, keyPath, key.Group)
require.Contains(t, keyPath, key.Resource)
require.Contains(t, keyPath, key.Namespace)
require.Contains(t, keyPath, key.Name)
// Verify action suffix
require.Contains(t, keyPath, fmt.Sprintf("~%s~", action))
// Verify snowflake calculation
expectedSnowflake := (((resourceVersion / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (resourceVersion % 1000)
require.Contains(t, keyPath, fmt.Sprintf("/%d~", expectedSnowflake), "actual RV: %d", actualRV)
// Verify folder if specified
if expectedFolder != "" {
require.Equal(t, expectedFolder, actualFolder)
require.Contains(t, keyPath, expectedFolder)
}
// Verify action code matches
var expectedActionCode int
switch action {
case "created":
expectedActionCode = 1
case "updated":
expectedActionCode = 2
case "deleted":
expectedActionCode = 3
}
require.Equal(t, expectedActionCode, actualAction)
}
// getAnnotationsJSON returns the annotations JSON string for the folder annotation if needed
func getAnnotationsJSON(withFolder bool) string {
if withFolder {
return `,
"annotations": {
"grafana.app/folder": "test-folder"
}`
}
return ""
}
// runTestSQLBackendFieldsCompatibility tests that KV backend with RvManager populates all SQL backend legacy fields
func runTestSQLBackendFieldsCompatibility(t *testing.T, sqlBackend, kvBackend resource.StorageBackend, nsPrefix string, db sqldb.DB) {
ctx := testutil.NewDefaultTestContext(t)
// Create unique namespace for isolation
namespace := nsPrefix + "-fields-test"
// Test SQL backend with 3 resources through complete lifecycle
t.Run("SQL Backend Operations", func(t *testing.T) {
runSQLBackendFieldsTest(t, sqlBackend, namespace+"-sql", db, ctx)
})
// Test KV backend with 3 resources through complete lifecycle
t.Run("KV Backend Operations", func(t *testing.T) {
runSQLBackendFieldsTest(t, kvBackend, namespace+"-kv", db, ctx)
})
}
// buildCrossDatabaseQuery converts query placeholders for different database drivers
func buildCrossDatabaseQuery(driverName, baseQuery string) string {
if driverName == "postgres" {
// Convert ? placeholders to $1, $2, etc. for PostgreSQL
placeholderCount := 1
result := baseQuery
for {
oldResult := result
result = strings.Replace(result, "?", fmt.Sprintf("$%d", placeholderCount), 1)
if result == oldResult {
break
}
placeholderCount++
}
return result
}
// MySQL and SQLite use ? placeholders
return baseQuery
}
// runSQLBackendFieldsTest performs complete resource lifecycle testing and verifies all legacy SQL fields
func runSQLBackendFieldsTest(t *testing.T, backend resource.StorageBackend, namespace string, db sqldb.DB, ctx context.Context) {
// Create storage server from backend
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
Backend: backend,
AccessClient: claims.FixedAccessClient(true), // Allow all operations for testing
})
require.NoError(t, err)
// Resource definitions with different folder configurations
resources := []struct {
name string
folder string
}{
{"test-resource-1", ""}, // No folder
{"test-resource-2", "test-folder"}, // With folder
{"test-resource-3", ""}, // No folder
}
// Track resource versions for each resource
resourceVersions := make([][]int64, len(resources)) // [resourceIndex][versionIndex]
// Create 3 resources
for i, res := range resources {
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: namespace,
Name: res.name,
}
// Create resource JSON with folder annotation and generation=1 for creates
resourceJSON := fmt.Sprintf(`{
"apiVersion": "playlist.grafana.app/v0alpha1",
"kind": "Playlist",
"metadata": {
"name": "%s",
"namespace": "%s",
"uid": "test-uid-%d",
"generation": 1%s
},
"spec": {
"title": "Test Playlist %d"
}
}`, res.name, namespace, i+1, getAnnotationsJSON(res.folder != ""), i+1)
// Create the resource
created, err := server.Create(ctx, &resourcepb.CreateRequest{
Key: key,
Value: []byte(resourceJSON),
})
require.NoError(t, err)
require.Nil(t, created.Error)
require.Greater(t, created.ResourceVersion, int64(0))
// Store the resource version
resourceVersions[i] = append(resourceVersions[i], created.ResourceVersion)
}
// Update 3 resources
for i, res := range resources {
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: namespace,
Name: res.name,
}
// Update resource JSON with generation=2 for updates
resourceJSON := fmt.Sprintf(`{
"apiVersion": "playlist.grafana.app/v0alpha1",
"kind": "Playlist",
"metadata": {
"name": "%s",
"namespace": "%s",
"uid": "test-uid-%d",
"generation": 2%s
},
"spec": {
"title": "Updated Test Playlist %d"
}
}`, res.name, namespace, i+1, getAnnotationsJSON(res.folder != ""), i+1)
// Update the resource using the current resource version
currentRV := resourceVersions[i][len(resourceVersions[i])-1]
updated, err := server.Update(ctx, &resourcepb.UpdateRequest{
Key: key,
Value: []byte(resourceJSON),
ResourceVersion: currentRV,
})
require.NoError(t, err)
require.Nil(t, updated.Error)
require.Greater(t, updated.ResourceVersion, currentRV)
// Store the new resource version
resourceVersions[i] = append(resourceVersions[i], updated.ResourceVersion)
}
// Delete first 2 resources (leave the last one to validate resource table)
for i, res := range resources[:2] {
key := &resourcepb.ResourceKey{
Group: "playlist.grafana.app",
Resource: "playlists",
Namespace: namespace,
Name: res.name,
}
// Delete the resource using the current resource version
currentRV := resourceVersions[i][len(resourceVersions[i])-1]
deleted, err := server.Delete(ctx, &resourcepb.DeleteRequest{
Key: key,
ResourceVersion: currentRV,
})
require.NoError(t, err)
require.Nil(t, deleted.Error)
require.Greater(t, deleted.ResourceVersion, currentRV)
// Store the delete resource version
resourceVersions[i] = append(resourceVersions[i], deleted.ResourceVersion)
}
// Verify all legacy SQL fields are populated correctly
verifyResourceHistoryTable(t, db, namespace, resources, resourceVersions)
verifyResourceTable(t, db, namespace, resources, resourceVersions)
verifyResourceVersionTable(t, db, namespace, resources, resourceVersions)
}
// ResourceHistoryRecord represents a row from the resource_history table
type ResourceHistoryRecord struct {
GUID string
Group string
Resource string
Namespace string
Name string
Value string
Action int
Folder string
PreviousResourceVersion int64
Generation int
ResourceVersion int64
}
// ResourceRecord represents a row from the resource table
type ResourceRecord struct {
GUID string
Group string
Resource string
Namespace string
Name string
Value string
Action int
Folder string
PreviousResourceVersion int64
ResourceVersion int64
}
// ResourceVersionRecord represents a row from the resource_version table
type ResourceVersionRecord struct {
Group string
Resource string
ResourceVersion int64
}
// verifyResourceHistoryTable validates all resource_history entries
func verifyResourceHistoryTable(t *testing.T, db sqldb.DB, namespace string, resources []struct{ name, folder string }, resourceVersions [][]int64) {
ctx := t.Context()
query := buildCrossDatabaseQuery(db.DriverName(), `
SELECT guid, "group", resource, namespace, name, value, action, folder,
previous_resource_version, generation, resource_version
FROM resource_history
WHERE namespace = ?
ORDER BY resource_version ASC
`)
rows, err := db.QueryContext(ctx, query, namespace)
require.NoError(t, err)
defer func() {
_ = rows.Close()
}()
var records []ResourceHistoryRecord
for rows.Next() {
var record ResourceHistoryRecord
err := rows.Scan(
&record.GUID, &record.Group, &record.Resource, &record.Namespace, &record.Name,
&record.Value, &record.Action, &record.Folder, &record.PreviousResourceVersion,
&record.Generation, &record.ResourceVersion,
)
require.NoError(t, err)
records = append(records, record)
}
require.NoError(t, rows.Err())
// We expect 8 records total: 3 creates + 3 updates + 2 deletes
require.Len(t, records, 8, "Expected 8 resource_history records (3 creates + 3 updates + 2 deletes)")
// Verify each record - we'll validate in the order they were created (by resource_version)
// The records are already sorted by resource_version ASC, so we just need to verify each one
recordIndex := 0
for resourceIdx, res := range resources {
// Check create record (action=1, generation=1)
createRecord := records[recordIndex]
verifyResourceHistoryRecord(t, createRecord, res, resourceIdx, 1, 0, 1, resourceVersions[resourceIdx][0])
recordIndex++
}
for resourceIdx, res := range resources {
// Check update record (action=2, generation=2)
updateRecord := records[recordIndex]
verifyResourceHistoryRecord(t, updateRecord, res, resourceIdx, 2, resourceVersions[resourceIdx][0], 2, resourceVersions[resourceIdx][1])
recordIndex++
}
for resourceIdx, res := range resources[:2] {
// Check delete record (action=3, generation=0) - only first 2 resources were deleted
deleteRecord := records[recordIndex]
verifyResourceHistoryRecord(t, deleteRecord, res, resourceIdx, 3, resourceVersions[resourceIdx][1], 0, resourceVersions[resourceIdx][2])
recordIndex++
}
}
// verifyResourceHistoryRecord validates a single resource_history record
func verifyResourceHistoryRecord(t *testing.T, record ResourceHistoryRecord, expectedRes struct{ name, folder string }, resourceIdx, expectedAction int, expectedPrevRV int64, expectedGeneration int, expectedRV int64) {
// Validate GUID (should be non-empty)
require.NotEmpty(t, record.GUID, "GUID should not be empty")
// Validate group/resource/namespace/name
require.Equal(t, "playlist.grafana.app", record.Group)
require.Equal(t, "playlists", record.Resource)
require.Equal(t, expectedRes.name, record.Name)
// Validate value contains expected JSON - server modifies/formats the JSON differently for different operations
// Check for both formats (with and without space after colon)
nameFound := strings.Contains(record.Value, fmt.Sprintf(`"name": "%s"`, expectedRes.name)) ||
strings.Contains(record.Value, fmt.Sprintf(`"name":"%s"`, expectedRes.name))
require.True(t, nameFound, "JSON should contain the expected name field")
kindFound := strings.Contains(record.Value, `"kind": "Playlist"`) ||
strings.Contains(record.Value, `"kind":"Playlist"`)
require.True(t, kindFound, "JSON should contain the expected kind field")
// Validate action
require.Equal(t, expectedAction, record.Action)
// Validate folder
if expectedRes.folder == "" {
require.Equal(t, "", record.Folder, "Folder should be empty when no folder annotation")
} else {
require.Equal(t, expectedRes.folder, record.Folder, "Folder should match annotation")
}
// Validate previous_resource_version
// For KV backend operations, resource versions are stored as snowflake format
// but expectedPrevRV is in microsecond format, so we need to use IsRvEqual for comparison
if strings.Contains(record.Namespace, "-kv") {
require.True(t, rvmanager.IsRvEqual(record.PreviousResourceVersion, expectedPrevRV),
"Previous resource version should match (KV backend snowflake format)")
} else {
require.Equal(t, expectedPrevRV, record.PreviousResourceVersion)
}
// Validate generation: 1 for create, 2 for update, 0 for delete
require.Equal(t, expectedGeneration, record.Generation)
// Validate resource_version
// For KV backend operations, resource versions are stored as snowflake format
if strings.Contains(record.Namespace, "-kv") {
require.True(t, rvmanager.IsRvEqual(record.ResourceVersion, expectedRV),
"Resource version should match (KV backend snowflake format)")
} else {
require.Equal(t, expectedRV, record.ResourceVersion)
}
}
// verifyResourceTable validates the resource table (latest state only)
func verifyResourceTable(t *testing.T, db sqldb.DB, namespace string, resources []struct{ name, folder string }, resourceVersions [][]int64) {
ctx := t.Context()
query := buildCrossDatabaseQuery(db.DriverName(), `
SELECT guid, "group", resource, namespace, name, value, action, folder,
previous_resource_version, resource_version
FROM resource
WHERE namespace = ?
ORDER BY name ASC
`)
rows, err := db.QueryContext(ctx, query, namespace)
require.NoError(t, err)
defer func() {
_ = rows.Close()
}()
var records []ResourceRecord
for rows.Next() {
var record ResourceRecord
err := rows.Scan(
&record.GUID, &record.Group, &record.Resource, &record.Namespace, &record.Name,
&record.Value, &record.Action, &record.Folder, &record.PreviousResourceVersion,
&record.ResourceVersion,
)
require.NoError(t, err)
records = append(records, record)
}
require.NoError(t, rows.Err())
// We expect 1 record since only 2 resources were deleted (the 3rd remains)
require.Len(t, records, 1, "Expected 1 resource record since only 2 resources were deleted")
// Validate the remaining record (should be the 3rd resource after update)
record := records[0]
require.Equal(t, "playlist.grafana.app", record.Group)
require.Equal(t, "playlists", record.Resource)
require.Equal(t, "test-resource-3", record.Name)
// Should be an update action (2) - resource table stores latest action
require.Equal(t, 2, record.Action)
// Validate value contains expected JSON
nameFound := strings.Contains(record.Value, fmt.Sprintf(`"name": "%s"`, "test-resource-3")) ||
strings.Contains(record.Value, fmt.Sprintf(`"name":"%s"`, "test-resource-3"))
require.True(t, nameFound, "JSON should contain the expected name field")
kindFound := strings.Contains(record.Value, `"kind": "Playlist"`) ||
strings.Contains(record.Value, `"kind":"Playlist"`)
require.True(t, kindFound, "JSON should contain the expected kind field")
// Folder should be empty (3rd resource has no folder annotation)
require.Equal(t, "", record.Folder, "3rd resource should have no folder")
// GUID should be non-empty
require.NotEmpty(t, record.GUID, "GUID should not be empty")
// Resource version should match the expected version for test-resource-3 (updated version)
expectedRV := resourceVersions[2][1] // test-resource-3's update version
if strings.Contains(namespace, "-kv") {
require.True(t, rvmanager.IsRvEqual(record.ResourceVersion, expectedRV),
"Resource version should match (KV backend snowflake format)")
} else {
require.Equal(t, expectedRV, record.ResourceVersion)
}
}
// verifyResourceVersionTable validates the resource_version table
func verifyResourceVersionTable(t *testing.T, db sqldb.DB, namespace string, resources []struct{ name, folder string }, resourceVersions [][]int64) {
ctx := t.Context()
query := buildCrossDatabaseQuery(db.DriverName(), `
SELECT "group", resource, resource_version
FROM resource_version
WHERE "group" = ? AND resource = ?
`)
// Check that we have exactly one entry for playlist.grafana.app/playlists
rows, err := db.QueryContext(ctx, query, "playlist.grafana.app", "playlists")
require.NoError(t, err)
defer func() {
_ = rows.Close()
}()
var records []ResourceVersionRecord
for rows.Next() {
var record ResourceVersionRecord
err := rows.Scan(&record.Group, &record.Resource, &record.ResourceVersion)
require.NoError(t, err)
records = append(records, record)
}
require.NoError(t, rows.Err())
// We expect exactly 1 record for the group+resource combination
require.Len(t, records, 1, "Expected 1 resource_version record for playlist.grafana.app/playlists")
record := records[0]
require.Equal(t, "playlist.grafana.app", record.Group)
require.Equal(t, "playlists", record.Resource)
// Find the highest resource version across all resources
var maxRV int64
for _, rvs := range resourceVersions {
for _, rv := range rvs {
if rv > maxRV {
maxRV = rv
}
}
}
// The resource_version table should contain the latest RV for the group+resource
// It might be slightly higher due to RV manager operations, so check it's at least our max
require.GreaterOrEqual(t, record.ResourceVersion, maxRV, "resource_version should be at least the latest RV we tracked")
// But it shouldn't be too much higher (within a reasonable range)
require.LessOrEqual(t, record.ResourceVersion, maxRV+100, "resource_version shouldn't be much higher than expected")
}

View File

@@ -7,7 +7,11 @@ import (
badger "github.com/dgraph-io/badger/v4"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
)
func TestBadgerKVStorageBackend(t *testing.T) {
@@ -37,35 +41,48 @@ func TestBadgerKVStorageBackend(t *testing.T) {
}
func TestSQLKVStorageBackend(t *testing.T) {
skipTests := map[string]bool{
TestHappyPath: true,
TestWatchWriteEvents: true,
TestList: true,
TestBlobSupport: true,
TestGetResourceStats: true,
TestListHistory: true,
TestListHistoryErrorReporting: true,
TestListModifiedSince: true,
TestListTrash: true,
TestCreateNewResource: true,
TestGetResourceLastImportTime: true,
TestOptimisticLocking: true,
newBackendFunc := func(ctx context.Context) (resource.StorageBackend, sqldb.DB) {
dbstore := db.InitTestDB(t)
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
require.NoError(t, err)
kv, err := resource.NewSQLKV(eDB)
require.NoError(t, err)
kvOpts := resource.KVBackendOptions{
KvStore: kv,
}
backend, err := resource.NewKVStorageBackend(kvOpts)
require.NoError(t, err)
db, err := eDB.Init(ctx)
require.NoError(t, err)
return backend, db
}
// without RvManager
RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend {
backend, _ := NewTestSqlKvBackend(t, ctx, false)
backend, _ := newBackendFunc(ctx)
return backend
}, &TestOptions{
NSPrefix: "sqlkvstorage-test",
SkipTests: skipTests,
NSPrefix: "sqlkvstorage-test",
SkipTests: map[string]bool{
TestHappyPath: true,
TestWatchWriteEvents: true,
TestList: true,
TestBlobSupport: true,
TestGetResourceStats: true,
TestListHistory: true,
TestListHistoryErrorReporting: true,
TestListModifiedSince: true,
TestListTrash: true,
TestCreateNewResource: true,
TestGetResourceLastImportTime: true,
TestOptimisticLocking: true,
TestKeyPathGeneration: true,
},
})
// with RvManager
RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend {
backend, _ := NewTestSqlKvBackend(t, ctx, true)
return backend
}, &TestOptions{
NSPrefix: "sqlkvstorage-withrvmanager-test",
SkipTests: skipTests,
RunSQLStorageBackendCompatibilityTest(t, newBackendFunc, &TestOptions{
NSPrefix: "sqlkvstorage-compatibility-test",
SkipTests: map[string]bool{
TestKeyPathGeneration: true,
},
})
}

View File

@@ -8,7 +8,6 @@ import (
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -133,94 +132,6 @@ func TestIntegrationDashboardAPIValidation(t *testing.T) {
}
}
func TestIntegrationDashboardAPIZanzana(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableDataMigrations: true,
AppModeProduction: true,
DisableAnonymous: true,
DisableAuthZClientCache: true,
DisableZanzanaCache: true,
DisableZanzanaServerCheckQueryCache: true,
ZanzanaReconciliationInterval: 1 * time.Second,
APIServerStorageType: "unified",
DBMaxConns: 10,
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {
DualWriterMode: rest.Mode5,
},
"folders.folder.grafana.app": {
DualWriterMode: rest.Mode5,
},
},
EnableFeatureToggles: []string{
"zanzana",
"zanzanaNoLegacyClient",
"kubernetesAuthzZanzanaSync",
},
UnifiedStorageEnableSearch: true,
})
t.Cleanup(func() {
helper.Shutdown()
})
org1Ctx := createTestContext(t, helper, helper.Org1, rest.Mode5)
org2Ctx := createTestContext(t, helper, helper.OrgB, rest.Mode5)
t.Run("Dashboard permission tests", func(t *testing.T) {
runDashboardPermissionTests(t, org1Ctx, true)
})
t.Run("Authorization tests for all identity types", func(t *testing.T) {
runAuthorizationTests(t, org1Ctx)
})
t.Run("Dashboard HTTP API test", func(t *testing.T) {
runDashboardHttpTest(t, org1Ctx, org2Ctx)
})
t.Run("Cross-organization tests", func(t *testing.T) {
runCrossOrgTests(t, org1Ctx, org2Ctx)
})
}
// list tests will go very slowly if the cache is disabled - allow the cache solely for Lists
func TestIntegrationDashboardAPIZanzanaList(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableDataMigrations: true,
AppModeProduction: true,
DisableAnonymous: true,
APIServerStorageType: "unified",
DBMaxConns: 4,
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {
DualWriterMode: rest.Mode5,
},
"folders.folder.grafana.app": {
DualWriterMode: rest.Mode5,
},
},
EnableFeatureToggles: []string{
"zanzana",
"zanzanaNoLegacyClient",
"kubernetesAuthzZanzanaSync",
},
UnifiedStorageEnableSearch: true,
ZanzanaReconciliationInterval: 100 * time.Millisecond,
})
t.Cleanup(func() {
helper.Shutdown()
})
org1Ctx := createTestContext(t, helper, helper.Org1, rest.Mode5)
runDashboardListTests(t, org1Ctx)
}
// TestIntegrationDashboardAPI tests the dashboard K8s API
func TestIntegrationDashboardAPI(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
@@ -300,11 +211,11 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Run("reject dashboard with existing UID", func(t *testing.T) {
// Create a dashboard with a specific UID
specificUID := "existing-uid-dash"
createdDash, err := createDashboard(t, adminClient, "Dashboard with Specific UID", nil, &specificUID, ctx.Helper)
createdDash, err := createDashboard(t, adminClient, "Dashboard with Specific UID", nil, &specificUID)
require.NoError(t, err)
// Try to create another dashboard with the same UID
_, err = createDashboard(t, adminClient, "Another Dashboard with Same UID", nil, &specificUID, ctx.Helper)
_, err = createDashboard(t, adminClient, "Another Dashboard with Same UID", nil, &specificUID)
require.Error(t, err)
// Clean up
@@ -316,14 +227,14 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Run("reject dashboard with too long UID", func(t *testing.T) {
// Create a dashboard with a long UID (over 40 chars)
longUID := "this-uid-is-way-too-long-for-a-dashboard-uid-12345678901234567890"
_, err := createDashboard(t, adminClient, "Dashboard with Long UID", nil, &longUID, ctx.Helper)
_, err := createDashboard(t, adminClient, "Dashboard with Long UID", nil, &longUID)
require.Error(t, err)
})
// Test creating dashboard with invalid UID characters
t.Run("reject dashboard with invalid UID characters", func(t *testing.T) {
invalidUID := "invalid/uid/with/slashes"
_, err := createDashboard(t, adminClient, "Dashboard with Invalid UID", nil, &invalidUID, ctx.Helper)
_, err := createDashboard(t, adminClient, "Dashboard with Invalid UID", nil, &invalidUID)
require.Error(t, err)
})
})
@@ -332,21 +243,21 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Run("Dashboard title validations", func(t *testing.T) {
// Test empty title
t.Run("reject dashboard with empty title", func(t *testing.T) {
_, err := createDashboard(t, adminClient, "", nil, nil, ctx.Helper)
_, err := createDashboard(t, adminClient, "", nil, nil)
require.Error(t, err)
})
// Test long title
t.Run("reject dashboard with excessively long title", func(t *testing.T) {
veryLongTitle := strings.Repeat("a", 10000)
_, err := createDashboard(t, adminClient, veryLongTitle, nil, nil, ctx.Helper)
_, err := createDashboard(t, adminClient, veryLongTitle, nil, nil)
require.Error(t, err)
})
// Test updating dashboard with empty title
t.Run("reject dashboard update with empty title", func(t *testing.T) {
// First create a valid dashboard
dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -362,7 +273,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
// Test updating dashboard with excessively long title
t.Run("reject dashboard update with excessively long title", func(t *testing.T) {
// First create a valid dashboard
dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -380,7 +291,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Run("Dashboard message validations", func(t *testing.T) {
// Test long message
t.Run("reject dashboard with excessively long update message", func(t *testing.T) {
dash, err := createDashboard(t, adminClient, "Regular dashboard", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Regular dashboard", nil, nil)
require.NoError(t, err)
veryLongMessage := strings.Repeat("a", 600)
@@ -393,78 +304,18 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
})
})
t.Run("Dashboard tag validations", func(t *testing.T) {
t.Run("reject dashboard with tag over 50 characters on creation", func(t *testing.T) {
dashObj := createDashboardObject(t, "Dashboard with Long Tag", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{"this-is-a-very-long-tag-that-exceeds-fifty-characters-limit"}
_ = meta.SetSpec(specMap)
_, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "tag too long")
})
t.Run("reject dashboard update with tag over 50 characters", func(t *testing.T) {
dash, err := createDashboard(t, adminClient, "Valid Dashboard", nil, nil, ctx.Helper)
require.NoError(t, err)
require.NotNil(t, dash)
meta, _ := utils.MetaAccessor(dash)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{"this-is-a-very-long-tag-that-exceeds-fifty-characters-limit"}
_ = meta.SetSpec(specMap)
_, err = adminClient.Resource.Update(context.Background(), dash, v1.UpdateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "tag too long")
err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
t.Run("accept dashboard with tag at 50 characters", func(t *testing.T) {
dashObj := createDashboardObject(t, "Dashboard with Valid Tag", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{"this-tag-is-exactly-fifty-characters-long-12345"}
_ = meta.SetSpec(specMap)
createdDash, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, createdDash)
err = adminClient.Resource.Delete(context.Background(), createdDash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
t.Run("reject dashboard with multiple tags where one exceeds limit", func(t *testing.T) {
dashObj := createDashboardObject(t, "Dashboard with Mixed Tags", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["tags"] = []string{
"valid-tag",
"another-valid-tag",
"this-is-a-very-long-tag-that-exceeds-fifty-characters-limit",
}
_ = meta.SetSpec(specMap)
_, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "tag too long")
})
})
t.Run("Dashboard folder validations", func(t *testing.T) {
// Test non-existent folder UID
t.Run("reject dashboard with non-existent folder UID", func(t *testing.T) {
nonExistentFolderUID := "non-existent-folder-uid"
_, err := createDashboard(t, adminClient, "Dashboard in Non-existent Folder", &nonExistentFolderUID, nil, ctx.Helper)
_, err := createDashboard(t, adminClient, "Dashboard in Non-existent Folder", &nonExistentFolderUID, nil)
ctx.Helper.EnsureStatusError(err, http.StatusNotFound, "folders.folder.grafana.app \"non-existent-folder-uid\" not found")
})
t.Run("allow moving folder to general folder", func(t *testing.T) {
folder1 := createFolderObject(t, "folder1", "default", "")
folder1UID := folder1.GetName()
dash, err := createDashboard(t, adminClient, "Dashboard in a Folder", &folder1UID, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard in a Folder", &folder1UID, nil)
require.NoError(t, err)
generalFolderUID := ""
@@ -586,7 +437,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
// Test version increment on update
t.Run("version increments on dashboard update", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for Version Test", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard for Version Test", nil, nil)
require.NoError(t, err, "Failed to create dashboard for version test")
dashUID := dash.GetName()
@@ -613,7 +464,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
// Test generation conflict when updating concurrently
t.Run("reject update with version conflict", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for Version Conflict Test", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard for Version Conflict Test", nil, nil)
require.NoError(t, err, "Failed to create dashboard for version conflict test")
dashUID := dash.GetName()
@@ -666,7 +517,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Run("dashboard version history available, even for UIDs ending in hyphen", func(t *testing.T) {
dashboardUID := "test-dashboard-"
dash, err := createDashboard(t, adminClient, "Dashboard with uid ending in hyphen", nil, &dashboardUID, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard with uid ending in hyphen", nil, &dashboardUID)
require.NoError(t, err)
updatedDash, err := updateDashboard(t, adminClient, dash, "Updated dashboard with uid ending in hyphen", nil)
@@ -713,7 +564,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for Provisioning Test", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard for Provisioning Test", nil, nil)
require.NoError(t, err, "Failed to create dashboard for provisioning test")
dashUID := dash.GetName()
@@ -838,7 +689,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
// Create a dashboard with a specific UID to make it easier to manage
specificUID := "size-limit-test-dash"
dash, err := createDashboard(t, adminClient, "Dashboard Exceeding Size Limit", nil, &specificUID, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard Exceeding Size Limit", nil, &specificUID)
require.NoError(t, err)
meta, _ := utils.MetaAccessor(dash)
@@ -1026,11 +877,11 @@ func runQuotaTests(t *testing.T, ctx TestContext) {
require.NoError(t, err, "Failed to update quota")
// Create first dashboard - should succeed
dash1, err := createDashboard(t, adminClient, fmt.Sprintf("Quota Test Dashboard 1 (%s)", tc.name), nil, nil, ctx.Helper)
dash1, err := createDashboard(t, adminClient, fmt.Sprintf("Quota Test Dashboard 1 (%s)", tc.name), nil, nil)
require.NoError(t, err, "Failed to create first dashboard")
// Create second dashboard - should fail due to quota
_, err = createDashboard(t, adminClient, fmt.Sprintf("Quota Test Dashboard 2 (%s)", tc.name), nil, nil, ctx.Helper)
_, err = createDashboard(t, adminClient, fmt.Sprintf("Quota Test Dashboard 2 (%s)", tc.name), nil, nil)
require.Error(t, err, "Creating second dashboard should fail due to quota")
require.Contains(t, err.Error(), "quota", "Error should mention quota")
@@ -1060,8 +911,6 @@ func runQuotaTests(t *testing.T, ctx TestContext) {
// Helper function to create test context for an organization
func createTestContext(t *testing.T, helper *apis.K8sTestHelper, orgUsers apis.OrgUsers, dualWriterMode rest.DualWriterMode) TestContext {
apis.AwaitZanzanaReconcileNext(t, helper)
// Create test folder
folderTitle := "Test Folder Org " + strconv.FormatInt(orgUsers.Admin.Identity.GetOrgID(), 10)
testFolder, err := createFolder(t, helper, orgUsers.Admin, folderTitle)
@@ -1164,8 +1013,6 @@ func createFolder(t *testing.T, helper *apis.K8sTestHelper, user apis.User, titl
return nil, err
}
apis.AwaitZanzanaReconcileNext(t, helper)
meta, _ := utils.MetaAccessor(createdFolder)
// Create a folder struct to return (for compatibility with existing code)
@@ -1240,7 +1087,7 @@ func markDashboardObjectAsProvisioned(t *testing.T, dashboard *unstructured.Unst
}
// Create a dashboard
func createDashboard(t *testing.T, client *apis.K8sResourceClient, title string, folderUID *string, uid *string, helper *apis.K8sTestHelper) (*unstructured.Unstructured, error) {
func createDashboard(t *testing.T, client *apis.K8sResourceClient, title string, folderUID *string, uid *string) (*unstructured.Unstructured, error) {
t.Helper()
var folderUIDStr string
@@ -1264,8 +1111,6 @@ func createDashboard(t *testing.T, client *apis.K8sResourceClient, title string,
return nil, err
}
apis.AwaitZanzanaReconcileNext(t, helper)
// Fetch the generated object to ensure we're not running into any caching or UID mismatch issues
databaseDash, err := client.Resource.Get(context.Background(), createdDash.GetName(), v1.GetOptions{})
if err != nil {
@@ -1409,13 +1254,11 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) {
{name: "in folder", folderUID: ctx.TestFolder.UID},
}
apis.AwaitZanzanaReconcileNext(t, ctx.Helper)
for _, loc := range locations {
t.Run(loc.name, func(t *testing.T) {
if roleCapabilities.canCreate {
// Test can create dashboard
dash, err := createDashboard(t, identity.DashboardClient, identity.Name+" Dashboard "+loc.name, &loc.folderUID, nil, ctx.Helper)
dash, err := createDashboard(t, identity.DashboardClient, identity.Name+" Dashboard "+loc.name, &loc.folderUID, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -1431,7 +1274,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) {
require.NoError(t, err)
} else {
// Test cannot create dashboard
_, err := createDashboard(t, identity.DashboardClient, identity.Name+" Dashboard "+loc.name, nil, nil, ctx.Helper)
_, err := createDashboard(t, identity.DashboardClient, identity.Name+" Dashboard "+loc.name, nil, nil)
require.Error(t, err)
}
})
@@ -1441,7 +1284,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) {
// Test dashboard updates
t.Run("dashboard update", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard to Update by "+identity.Name, nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard to Update by "+identity.Name, nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -1468,7 +1311,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) {
// Test dashboard deletion permissions
t.Run("dashboard deletion", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for deletion test by "+identity.Name, nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard for deletion test by "+identity.Name, nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -1488,7 +1331,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) {
// Test dashboard viewing for all roles
t.Run("dashboard viewing", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for "+identity.Name+" to view", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard for "+identity.Name+" to view", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -1520,7 +1363,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
// Test custom dashboard permissions
t.Run("Dashboard with custom permissions", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard with Custom Permissions", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard with Custom Permissions", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -1551,12 +1394,12 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
// Test dashboard-specific permission overrides (new test case)
t.Run("Dashboard-specific permission overrides", func(t *testing.T) {
// Create multiple dashboards with admin
dash1, err := createDashboard(t, adminClient, "Dashboard with No Custom Permissions", nil, nil, ctx.Helper)
dash1, err := createDashboard(t, adminClient, "Dashboard with No Custom Permissions", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash1)
dash1UID := dash1.GetName()
dash2, err := createDashboard(t, adminClient, "Dashboard with Viewer Edit Permission", nil, nil, ctx.Helper)
dash2, err := createDashboard(t, adminClient, "Dashboard with Viewer Edit Permission", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash2)
dash2UID := dash2.GetName()
@@ -1600,7 +1443,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
setResourceUserPermission(t, ctx, ctx.AdminUser, false, folderUID, addUserPermission(t, nil, ctx.ViewerUser, ResourcePermissionLevelEdit))
// Create a dashboard in the folder with admin
dash, err := createDashboard(t, adminClient, "Dashboard in Custom Permission Folder", &folderUID, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard in Custom Permission Folder", &folderUID, nil)
require.NoError(t, err)
require.NotNil(t, dash)
@@ -1619,7 +1462,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
require.Equal(t, "Updated by Viewer with Folder Permission", meta.FindTitle(""))
// User should be able to create a dashboard in the folder
dashViewer, err := createDashboard(t, viewerClient, "Dashboard created by Viewer in Custom Permission Folder", &folderUID, nil, ctx.Helper)
dashViewer, err := createDashboard(t, viewerClient, "Dashboard created by Viewer in Custom Permission Folder", &folderUID, nil)
require.NoError(t, err)
require.NotNil(t, dashViewer)
@@ -1666,7 +1509,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
setResourceUserPermission(t, ctx, ctx.AdminUser, false, folder2UID, addUserPermission(t, nil, ctx.ViewerUser, ResourcePermissionLevelEdit))
// Have the viewer create a dashboard in folder2
viewerDash, err := createDashboard(t, viewerClient, "Dashboard created by Viewer in Edit Permission Folder", &folder2UID, nil, ctx.Helper)
viewerDash, err := createDashboard(t, viewerClient, "Dashboard created by Viewer in Edit Permission Folder", &folder2UID, nil)
require.NoError(t, err, "Viewer should be able to create dashboard in folder with edit permissions")
require.NotNil(t, viewerDash)
dashUID := viewerDash.GetName()
@@ -1701,7 +1544,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
// Test creator permissions (new test case)
t.Run("Creator of dashboard gets admin permission", func(t *testing.T) {
// Create a dashboard as an editor user (not admin)
editorCreatedDash, err := createDashboard(t, editorClient, "Dashboard Created by Editor", nil, nil, ctx.Helper)
editorCreatedDash, err := createDashboard(t, editorClient, "Dashboard Created by Editor", nil, nil)
require.NoError(t, err)
require.NotNil(t, editorCreatedDash)
dashUID := editorCreatedDash.GetName()
@@ -1732,7 +1575,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
t.Run("Admin can override creator permissions", func(t *testing.T) {
t.Skip("Have to double check if that's actually the case")
// Create a dashboard as an editor user (not admin)
editorCreatedDash, err := createDashboard(t, editorClient, "Dashboard Created by Editor for Permission Test", nil, nil, ctx.Helper)
editorCreatedDash, err := createDashboard(t, editorClient, "Dashboard Created by Editor for Permission Test", nil, nil)
require.NoError(t, err)
require.NotNil(t, editorCreatedDash)
dashUID := editorCreatedDash.GetName()
@@ -1771,7 +1614,7 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashbo
otherOrgClient := getResourceClient(t, ctx.Helper, ctx.Helper.OrgB.Viewer, getDashboardGVR())
// Create a dashboard with admin in the current org
dash, err := createDashboard(t, adminClient, "Dashboard for Cross-Org Permissions Test", nil, nil, ctx.Helper)
dash, err := createDashboard(t, adminClient, "Dashboard for Cross-Org Permissions Test", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
org1DashUID := dash.GetName()
@@ -1860,11 +1703,11 @@ func runCrossOrgTests(t *testing.T, org1Ctx, org2Ctx TestContext) {
dashTitle := "Cross-Org Dashboard"
// Create in org1
dash1, err := createDashboard(t, org1SuperAdminClient, dashTitle, nil, &uid, org1Ctx.Helper)
dash1, err := createDashboard(t, org1SuperAdminClient, dashTitle, nil, &uid)
require.NoError(t, err, "Failed to create dashboard in org1")
// Create in org2 with same UID - should succeed (UIDs only need to be unique within an org)
dash2, err := createDashboard(t, org2SuperAdminClient, dashTitle, nil, &uid, org2Ctx.Helper)
dash2, err := createDashboard(t, org2SuperAdminClient, dashTitle, nil, &uid)
require.NoError(t, err, "Failed to create dashboard with same UID in org2")
// Verify both dashboards were created
@@ -1950,12 +1793,12 @@ func runCrossOrgTests(t *testing.T, org1Ctx, org2Ctx TestContext) {
// Test cross-organization access
t.Run("Cross-organization access", func(t *testing.T) {
// Create dashboards in both orgs
org1Dashboard, err := createDashboard(t, org1SuperAdminClient, "Org1 Dashboard", nil, nil, org1Ctx.Helper)
org1Dashboard, err := createDashboard(t, org1SuperAdminClient, "Org1 Dashboard", nil, nil)
require.NoError(t, err)
require.NotNil(t, org1Dashboard)
org1DashUID := org1Dashboard.GetName()
org2Dashboard, err := createDashboard(t, org2SuperAdminClient, "Org2 Dashboard", nil, nil, org2Ctx.Helper)
org2Dashboard, err := createDashboard(t, org2SuperAdminClient, "Org2 Dashboard", nil, nil)
require.NoError(t, err)
require.NotNil(t, org2Dashboard)
org2DashUID := org2Dashboard.GetName()
@@ -2114,8 +1957,6 @@ func setResourceUserPermission(t *testing.T, ctx TestContext, actingUser apis.Us
// Check response status code
require.Equal(t, http.StatusOK, resp.Response.StatusCode, "Failed to set permissions for %s", resourceUID)
apis.AwaitZanzanaReconcileNext(t, ctx.Helper)
}
// Test creating a dashboard via HTTP and deleting it
@@ -2192,7 +2033,6 @@ func runDashboardHttpTest(t *testing.T, ctx TestContext, foreignOrgCtx TestConte
for _, userTC := range userTestCases {
testName := fmt.Sprintf("%s by %s", locTC.name, userTC.name)
t.Run(testName, func(t *testing.T) {
apis.AwaitZanzanaReconcileNext(t, ctx.Helper)
// Create a unique dashboard UID - ensure it's 40 chars max
dashboardUID := fmt.Sprintf("test-%s-%s-%s",
"POST",
@@ -2238,8 +2078,6 @@ func runDashboardHttpTest(t *testing.T, ctx TestContext, foreignOrgCtx TestConte
ContentType: "application/json",
}, &struct{}{})
apis.AwaitZanzanaReconcileNext(t, ctx.Helper)
// Check if the creation was successful or failed as expected
adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR())
@@ -2583,7 +2421,7 @@ func runDashboardListTests(t *testing.T, ctx TestContext) {
// Create all test resources (folders, dashboards) in one loop
for i, fc := range folderConfigs {
// Create root dashboard
rootDash, err := createDashboard(t, adminClient, fmt.Sprintf("Root Dashboard - %s", fc.name), nil, nil, ctx.Helper)
rootDash, err := createDashboard(t, adminClient, fmt.Sprintf("Root Dashboard - %s", fc.name), nil, nil)
require.NoError(t, err)
rootDashboards[i] = rootDash
fc.permissions(t, ctx, rootDash.GetName(), true)
@@ -2595,7 +2433,7 @@ func runDashboardListTests(t *testing.T, ctx TestContext) {
fc.permissions(t, ctx, folder.UID, false)
// Create dashboard in folder
folderDash, err := createDashboard(t, adminClient, fmt.Sprintf("Dashboard in %s folder", fc.name), &folder.UID, nil, ctx.Helper)
folderDash, err := createDashboard(t, adminClient, fmt.Sprintf("Dashboard in %s folder", fc.name), &folder.UID, nil)
require.NoError(t, err)
folderDashboards[i] = folderDash
}
@@ -2756,10 +2594,10 @@ func runDashboardTrashTests(t *testing.T, ctx TestContext) {
t.Run("regular dashboards appear in trash but provisioned ones do not", func(t *testing.T) {
// create two dashboards, one that is provisioned and one that is not
regularDash, err := createDashboard(t, adminClient, "Regular Dashboard for Trash Comparison", nil, nil, ctx.Helper)
regularDash, err := createDashboard(t, adminClient, "Regular Dashboard for Trash Comparison", nil, nil)
require.NoError(t, err)
regularDashUID := regularDash.GetName()
provisionedDash, err := createDashboard(t, adminClient, "Provisioned Dashboard for Trash Comparison", nil, nil, ctx.Helper)
provisionedDash, err := createDashboard(t, adminClient, "Provisioned Dashboard for Trash Comparison", nil, nil)
require.NoError(t, err)
provisionedDashUID := provisionedDash.GetName()
meta, err := utils.MetaAccessor(provisionedDash)
@@ -2788,7 +2626,7 @@ func runDashboardTrashTests(t *testing.T, ctx TestContext) {
})
t.Run("permission checks - admin can see everything, users can see their own deleted items", func(t *testing.T) {
dash, err := createDashboard(t, editorClient, "Dashboard for Trash Test", nil, nil, ctx.Helper)
dash, err := createDashboard(t, editorClient, "Dashboard for Trash Test", nil, nil)
require.NoError(t, err)
dashUID := dash.GetName()
err = editorClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{})

View File

@@ -36,12 +36,10 @@ func TestIntegrationFolderTreeZanzana(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
runIntegrationFolderTree(t, testinfra.GrafanaOpts{
DisableDataMigrations: true,
AppModeProduction: true,
DisableAnonymous: true,
DisableAuthZClientCache: true,
DisableZanzanaServerCheckQueryCache: true,
APIServerStorageType: "unified",
DisableDataMigrations: true,
AppModeProduction: true,
DisableAnonymous: true,
APIServerStorageType: "unified",
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {
DualWriterMode: grafanarest.Mode5,

View File

@@ -866,80 +866,6 @@
}
]
},
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/repositories": {
"get": {
"tags": [
"Connection"
],
"summary": "List external repositories",
"description": "List repositories available from the external git provider through this connection",
"operationId": "getConnectionRepositories",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"description": "ExternalRepositoryList lists repositories from an external git provider",
"type": "object",
"required": [
"items"
],
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"items": {
"type": "array",
"items": {
"default": {}
},
"x-kubernetes-list-type": "atomic"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"default": {}
}
}
}
}
}
}
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "provisioning.grafana.app",
"version": "v0alpha1",
"kind": "ExternalRepositoryList"
}
},
"parameters": [
{
"name": "name",
"in": "path",
"description": "name of the ExternalRepositoryList",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "namespace",
"in": "path",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
}
]
},
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/status": {
"get": {
"tags": [
@@ -4719,73 +4645,6 @@
}
}
},
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepository": {
"type": "object",
"required": [
"name",
"url"
],
"properties": {
"name": {
"description": "Name of the repository",
"type": "string",
"default": ""
},
"owner": {
"description": "Owner is the user, organization, or workspace that owns the repository For GitHub: organization or user For GitLab: namespace (user or group) For Bitbucket: workspace For pure Git: empty",
"type": "string"
},
"url": {
"description": "URL of the repository",
"type": "string",
"default": ""
}
}
},
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepositoryList": {
"description": "ExternalRepositoryList lists repositories from an external git provider",
"type": "object",
"required": [
"items"
],
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"items": {
"type": "array",
"items": {
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepository"
}
]
},
"x-kubernetes-list-type": "atomic"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
}
]
}
},
"x-kubernetes-group-version-kind": [
{
"group": "provisioning.grafana.app",
"kind": "ExternalRepositoryList",
"version": "v0alpha1"
}
]
},
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.FileItem": {
"type": "object",
"required": [

View File

@@ -67,7 +67,7 @@ func TestIntegrationPreferences(t *testing.T) {
Path: fmt.Sprintf("/api/teams/%d/preferences", helper.Org1.Staff.ID),
Body: []byte(`{
"weekStart": "sunday",
"timezone": "Africa/Johannesburg"
"timezone": "africa"
}`),
}, &raw)
require.Equal(t, http.StatusOK, legacyResponse.Response.StatusCode, "create preference for user")
@@ -79,7 +79,7 @@ func TestIntegrationPreferences(t *testing.T) {
Path: "/api/org/preferences",
Body: []byte(`{
"weekStart": "sunday",
"timezone": "Africa/Accra",
"timezone": "africa",
"theme": "dark"
}`),
}, &raw)
@@ -144,7 +144,7 @@ func TestIntegrationPreferences(t *testing.T) {
jj, _ = json.Marshal(bootdata.Result.User)
require.JSONEq(t, `{
"timezone":"Africa/Johannesburg",
"timezone":"africa",
"weekStart":"saturday",
"theme":"dark",
"language":"en-US", `+ // FROM global default!
@@ -157,10 +157,10 @@ func TestIntegrationPreferences(t *testing.T) {
Path: "/apis/preferences.grafana.app/v1alpha1/namespaces/default/preferences/merged",
}, &preferences.Preferences{})
require.Equal(t, http.StatusOK, merged.Response.StatusCode, "get merged preferences")
require.Equal(t, "saturday", *merged.Result.Spec.WeekStart) // from user
require.Equal(t, "Africa/Johannesburg", *merged.Result.Spec.Timezone) // from team
require.Equal(t, "dark", *merged.Result.Spec.Theme) // from org
require.Equal(t, "en-US", *merged.Result.Spec.Language) // settings.ini
require.Equal(t, "dd/mm/yyyy", *merged.Result.Spec.RegionalFormat) // from user update
require.Equal(t, "saturday", *merged.Result.Spec.WeekStart) // from user
require.Equal(t, "africa", *merged.Result.Spec.Timezone) // from team
require.Equal(t, "dark", *merged.Result.Spec.Theme) // from org
require.Equal(t, "en-US", *merged.Result.Spec.Language) // settings.ini
require.Equal(t, "dd/mm/yyyy", *merged.Result.Spec.RegionalFormat) // from user update
})
}

View File

@@ -1,172 +0,0 @@
package provisioning
import (
"context"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestIntegrationProvisioning_ConnectionRepositories(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
// Create a connection for testing
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection-repositories-test",
"namespace": "default",
},
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "123456",
"installationID": "454545",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.NoError(t, err, "failed to create connection")
t.Run("endpoint returns not implemented", func(t *testing.T) {
var statusCode int
result := helper.AdminREST.Get().
Namespace("default").
Resource("connections").
Name("connection-repositories-test").
SubResource("repositories").
Do(ctx).
StatusCode(&statusCode)
require.Error(t, result.Error(), "should return error for not implemented endpoint")
require.Equal(t, http.StatusMethodNotAllowed, statusCode, "should return 405 Method Not Allowed")
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
})
t.Run("admin can access endpoint (gets not implemented)", func(t *testing.T) {
var statusCode int
result := helper.AdminREST.Get().
Namespace("default").
Resource("connections").
Name("connection-repositories-test").
SubResource("repositories").
Do(ctx).StatusCode(&statusCode)
// Endpoint exists but returns not implemented
require.Error(t, result.Error(), "should return error")
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
// Status code should be 405 (Method Not Allowed) for method not supported
require.Equal(t, http.StatusMethodNotAllowed, statusCode)
})
t.Run("editor cannot access endpoint", func(t *testing.T) {
var statusCode int
result := helper.EditorREST.Get().
Namespace("default").
Resource("connections").
Name("connection-repositories-test").
SubResource("repositories").
Do(ctx).StatusCode(&statusCode)
require.Error(t, result.Error(), "editor should not be able to access repositories endpoint")
require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden")
require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden")
})
t.Run("viewer cannot access endpoint", func(t *testing.T) {
var statusCode int
result := helper.ViewerREST.Get().
Namespace("default").
Resource("connections").
Name("connection-repositories-test").
SubResource("repositories").
Do(ctx).StatusCode(&statusCode)
require.Error(t, result.Error(), "viewer should not be able to access repositories endpoint")
require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden")
require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden")
})
t.Run("non-GET methods are rejected", func(t *testing.T) {
configBytes, _ := json.Marshal(map[string]any{})
var statusCode int
result := helper.AdminREST.Post().
Namespace("default").
Resource("connections").
Name("connection-repositories-test").
SubResource("repositories").
Body(configBytes).
SetHeader("Content-Type", "application/json").
Do(ctx).StatusCode(&statusCode)
require.Error(t, result.Error(), "POST should not be allowed")
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
})
}
func TestIntegrationProvisioning_ConnectionRepositoriesResponseType(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
// Create a connection for testing
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection-repositories-type-test",
"namespace": "default",
},
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "123456",
"installationID": "454545",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.NoError(t, err, "failed to create connection")
t.Run("verify ExternalRepositoryList type exists in API", func(t *testing.T) {
// Verify the type is registered and can be instantiated
list := &provisioning.ExternalRepositoryList{}
require.NotNil(t, list)
// Verify it has the expected structure (Items is a slice, nil by default is fine)
require.IsType(t, []provisioning.ExternalRepository{}, list.Items)
// Can create items
list.Items = []provisioning.ExternalRepository{
{Name: "test", Owner: "owner", URL: "https://example.com/repo"},
}
require.Len(t, list.Items, 1)
require.Equal(t, "test", list.Items[0].Name)
})
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"testing"
"time"
"github.com/grafana/grafana/pkg/util/testutil"
"github.com/stretchr/testify/assert"
@@ -12,9 +11,6 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
clientset "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned"
)
func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
@@ -415,147 +411,3 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
assert.Contains(t, err.Error(), "privateKey is forbidden in Gitlab connection")
})
}
func TestIntegrationConnectionController_HealthCheckUpdates(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
namespace := "default"
// Create typed client from REST config
restConfig := helper.Org1.Admin.NewRestConfig()
provisioningClient, err := clientset.NewForConfig(restConfig)
require.NoError(t, err)
connClient := provisioningClient.ProvisioningV0alpha1().Connections(namespace)
t.Run("health check gets updated after initial creation", func(t *testing.T) {
// Create a connection using unstructured (like other connection tests)
connUnstructured := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "test-connection-health",
"namespace": namespace,
},
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "12345",
"installationID": "67890",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "test-private-key",
},
},
}}
createdUnstructured, err := helper.Connections.Resource.Create(ctx, connUnstructured, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, createdUnstructured)
connName := createdUnstructured.GetName()
t.Cleanup(func() {
_ = helper.Connections.Resource.Delete(ctx, connName, metav1.DeleteOptions{})
})
// Wait for initial reconciliation - controller should update status
require.Eventually(t, func() bool {
updated, err := connClient.Get(ctx, connName, metav1.GetOptions{})
if err != nil {
return false
}
return updated.Status.ObservedGeneration == updated.Generation &&
updated.Status.Health.Checked > 0 &&
updated.Status.State == provisioning.ConnectionStateConnected &&
updated.Status.Health.Healthy
}, 10*time.Second, 500*time.Millisecond, "connection should be initially reconciled with health status")
// Verify initial health check was set
initial, err := connClient.Get(ctx, connName, metav1.GetOptions{})
require.NoError(t, err)
assert.True(t, initial.Status.Health.Healthy, "connection should be healthy")
assert.Equal(t, provisioning.ConnectionStateConnected, initial.Status.State, "connection should be connected")
assert.Greater(t, initial.Status.Health.Checked, int64(0), "health check timestamp should be set")
assert.Equal(t, initial.Generation, initial.Status.ObservedGeneration, "observed generation should match")
})
t.Run("health check updates when spec changes", func(t *testing.T) {
// Create a connection using unstructured
connUnstructured := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "test-connection-spec-change",
"namespace": namespace,
},
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "11111",
"installationID": "22222",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "test-private-key-2",
},
},
}}
createdUnstructured, err := helper.Connections.Resource.Create(ctx, connUnstructured, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, createdUnstructured)
connName := createdUnstructured.GetName()
t.Cleanup(func() {
_ = helper.Connections.Resource.Delete(ctx, connName, metav1.DeleteOptions{})
})
// Wait for initial reconciliation
var initialHealthChecked int64
require.Eventually(t, func() bool {
updated, err := connClient.Get(ctx, connName, metav1.GetOptions{})
if err != nil {
return false
}
if updated.Status.ObservedGeneration == updated.Generation {
initialHealthChecked = updated.Status.Health.Checked
return true
}
return false
}, 10*time.Second, 500*time.Millisecond, "connection should be initially reconciled")
// Get the latest version before updating to avoid conflicts with controller updates
latestUnstructured, err := helper.Connections.Resource.Get(ctx, connName, metav1.GetOptions{})
require.NoError(t, err)
// Update the connection spec using the latest version
updatedUnstructured := latestUnstructured.DeepCopy()
githubSpec := updatedUnstructured.Object["spec"].(map[string]any)["github"].(map[string]any)
githubSpec["appID"] = "99999"
_, err = helper.Connections.Resource.Update(ctx, updatedUnstructured, metav1.UpdateOptions{})
require.NoError(t, err)
// Wait for reconciliation after spec change
require.Eventually(t, func() bool {
reconciled, err := connClient.Get(ctx, connName, metav1.GetOptions{})
if err != nil {
return false
}
return reconciled.Status.ObservedGeneration == reconciled.Generation &&
reconciled.Status.Health.Checked > initialHealthChecked
}, 10*time.Second, 500*time.Millisecond, "connection should be reconciled after spec change")
// Verify health check was updated
final, err := connClient.Get(ctx, connName, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, final.Generation, final.Status.ObservedGeneration, "observed generation should match generation")
assert.Greater(t, final.Status.Health.Checked, initialHealthChecked, "health check should be updated after spec change")
assert.True(t, final.Status.Health.Healthy, "connection should remain healthy")
})
}

View File

@@ -18,10 +18,7 @@ import (
const zanzanaReconcileLastSuccessMetric = "grafana_zanzana_reconcile_last_success_timestamp_seconds"
// AwaitZanzanaReconcileNext waits for a Zanzana reconciliation cycle whose last-success timestamp
// has been incremented from its current value. This ensures a reconciliation has occurred after
// this function is called.
//
// AwaitZanzanaReconcileNext waits for the next Zanzana reconciliation cycle to complete.
// It is a no-op unless the `zanzana` feature toggle is enabled for the running test env.
func AwaitZanzanaReconcileNext(t *testing.T, helper *K8sTestHelper) {
t.Helper()
@@ -34,14 +31,18 @@ func AwaitZanzanaReconcileNext(t *testing.T, helper *K8sTestHelper) {
return
}
baselineTimestamp, _ := getZanzanaReconcileLastSuccessTimestampSeconds(t, helper)
prev, ok := getZanzanaReconcileLastSuccessTimestampSeconds(t, helper)
if !ok {
prev = 0
}
require.EventuallyWithT(t, func(c *assert.CollectT) {
ts, ok := getZanzanaReconcileLastSuccessTimestampSeconds(t, helper)
assert.True(c, ok, "expected to find %s in /metrics", zanzanaReconcileLastSuccessMetric)
if !ok {
return
}
assert.Greater(c, ts, baselineTimestamp, "expected %s (%v) > baseline (%v)", zanzanaReconcileLastSuccessMetric, ts, baselineTimestamp)
assert.Greater(c, ts, prev, "expected %s (%v) > %v", zanzanaReconcileLastSuccessMetric, ts, prev)
}, 30*time.Second, 50*time.Millisecond)
}

View File

@@ -370,39 +370,6 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
require.NoError(t, err)
}
if opts.DisableZanzanaServerCheckQueryCache {
zanzanaServerSect, err := cfg.NewSection("zanzana.server")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("check_cache_limit", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("cache_controller_enabled", "false")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("cache_controller_ttl", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("check_query_cache_enabled", "false")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("check_query_cache_ttl", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("check_iterator_cache_enabled", "false")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("check_iterator_cache_max_results", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("check_iterator_cache_ttl", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("list_objects_iterator_cache_enabled", "false")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("list_objects_iterator_cache_max_results", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("list_objects_iterator_cache_ttl", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("shared_iterator_enabled", "false")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("shared_iterator_limit", "0")
require.NoError(t, err)
_, err = zanzanaServerSect.NewKey("shared_iterator_ttl", "0")
require.NoError(t, err)
}
analyticsSect, err := cfg.NewSection("analytics")
require.NoError(t, err)
_, err = analyticsSect.NewKey("intercom_secret", "intercom_secret_at_config")
@@ -674,14 +641,9 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
require.NoError(t, err)
_, err = dbSection.NewKey("query_retries", fmt.Sprintf("%d", queryRetries))
require.NoError(t, err)
maxConns := opts.DBMaxConns
if maxConns <= 0 {
maxConns = 2
}
_, err = dbSection.NewKey("max_open_conn", fmt.Sprintf("%d", maxConns))
_, err = dbSection.NewKey("max_open_conn", "2")
require.NoError(t, err)
_, err = dbSection.NewKey("max_idle_conn", fmt.Sprintf("%d", maxConns))
_, err = dbSection.NewKey("max_idle_conn", "2")
require.NoError(t, err)
cfgPath := filepath.Join(cfgDir, "test.ini")
@@ -744,10 +706,6 @@ type GrafanaOpts struct {
DisableAuthZClientCache bool
ZanzanaReconciliationInterval time.Duration
DisableZanzanaCache bool
DisableZanzanaServerCheckQueryCache bool
// If set to 0, the default (2) is used.
DBMaxConns int
// Allow creating grafana dir beforehand
Dir string

View File

@@ -6152,8 +6152,11 @@
]
},
"timezone": {
"description": "Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string",
"type": "string"
"type": "string",
"enum": [
"utc",
"browser"
]
},
"weekStart": {
"type": "string"
@@ -8654,8 +8657,11 @@
]
},
"timezone": {
"description": "Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string",
"type": "string"
"type": "string",
"enum": [
"utc",
"browser"
]
},
"weekStart": {
"type": "string"

14
public/api-merged.json generated
View File

@@ -18729,8 +18729,11 @@
]
},
"timezone": {
"description": "Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string",
"type": "string"
"type": "string",
"enum": [
"utc",
"browser"
]
},
"weekStart": {
"type": "string"
@@ -23117,8 +23120,11 @@
]
},
"timezone": {
"description": "Any IANA timezone string (e.g. America/New_York), 'utc', 'browser', or empty string",
"type": "string"
"type": "string",
"enum": [
"utc",
"browser"
]
},
"weekStart": {
"type": "string"

View File

@@ -1,4 +1,4 @@
import { EchoBackend, EchoMeta, EchoEvent, EchoSrv, reportInteraction } from '@grafana/runtime';
import { EchoBackend, EchoMeta, EchoEvent, EchoSrv } from '@grafana/runtime';
import { contextSrv } from '../context_srv';
@@ -90,15 +90,3 @@ export class Echo implements EchoSrv {
};
};
}
/** Analytics framework:
* Foundational types and functions for the new tracking event process
*/
export type TrackingEventProps = {
[key: string]: boolean | string | number | undefined;
};
export const createEventFactory = (product: string, featureName: string) => {
return <P extends TrackingEventProps | undefined = undefined>(eventName: string) =>
(props: P extends undefined ? void : P) =>
reportInteraction(`${product}_${featureName}_${eventName}`, props ?? undefined);
};

View File

@@ -7,13 +7,10 @@ import { SceneGridLayout, VizPanel, SceneVariableSet } from '@grafana/scenes';
import { activateFullSceneTree } from '../../utils/test-utils';
import { DashboardScene } from '../DashboardScene';
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabItem } from '../layout-tabs/TabItem';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { LayoutParent } from '../types/LayoutParent';
import { DashboardLayoutSelector } from './DashboardLayoutSelector';
@@ -43,27 +40,6 @@ describe('DashboardLayoutSelector', () => {
await user.click(confirmButton);
expect(switchLayoutMock).toHaveBeenCalled();
});
it('should disable tabs option when a row contains tabs layout and show correct message', async () => {
const scene = buildTestSceneWithNestedTabs();
const layoutManager = scene.state.body;
render(<DashboardLayoutSelector layoutManager={layoutManager} />);
const tabsOption = screen.getByLabelText('layout-selection-option-Tabs');
expect(tabsOption).toBeDisabled();
expect(screen.getByTitle('Cannot change to tabs because a row already contains tabs')).toBeInTheDocument();
});
it('should not disable tabs option when rows do not contain tabs', async () => {
const scene = buildTestScene();
const layoutManager = scene.state.body;
render(<DashboardLayoutSelector layoutManager={layoutManager} />);
const tabsOption = screen.getByLabelText('layout-selection-option-Tabs');
expect(tabsOption).not.toBeDisabled();
});
});
const buildTestScene = () => {
@@ -94,43 +70,3 @@ const buildTestScene = () => {
activateFullSceneTree(scene);
return scene;
};
const buildTestSceneWithNestedTabs = () => {
const scene = new DashboardScene({
title: 'testScene',
editable: true,
$variables: new SceneVariableSet({
variables: [],
}),
body: new RowsLayoutManager({
rows: [
new RowItem({
title: 'Row 1',
layout: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [
new DashboardGridItem({
body: new VizPanel({ key: 'panel-1', pluginId: 'text' }),
}),
],
}),
}),
}),
new RowItem({
title: 'Row with Tabs',
layout: new TabsLayoutManager({
tabs: [
new TabItem({
title: 'Tab 1',
layout: AutoGridLayoutManager.createEmpty(),
}),
],
}),
}),
],
}),
});
activateFullSceneTree(scene);
return scene;
};

View File

@@ -11,7 +11,6 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { isLayoutParent } from '../types/LayoutParent';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
import { containsTabsLayout } from './findAllGridTypes';
import { layoutRegistry } from './layoutRegistry';
export interface Props {
@@ -23,26 +22,19 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout);
const [newLayout, setNewLayout] = useState<LayoutRegistryItem | undefined>();
const disableTabsReason = useMemo(() => {
const disableTabs = useMemo(() => {
if (config.featureToggles.unlimitedLayoutsNesting) {
return undefined;
return false;
}
// Check parent hierarchy
let parent = layoutManager.parent;
while (parent) {
if (parent instanceof TabsLayoutManager) {
return 'parent';
return true;
}
parent = parent.parent;
}
// Check child hierarchy
if (containsTabsLayout(layoutManager)) {
return 'child';
}
return undefined;
return false;
}, [layoutManager]);
const onChangeLayout = useCallback((newLayout: LayoutRegistryItem) => setNewLayout(newLayout), []);
@@ -67,15 +59,8 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
const radioOptions = options.map((opt) => {
let description = opt.description;
if (disableTabsReason && opt.id === TabsLayoutManager.descriptor.id) {
if (disableTabsReason === 'parent') {
description = t('dashboard.canvas-actions.disabled-nested-tabs', 'Tabs cannot be nested inside other tabs');
} else {
description = t(
'dashboard.canvas-actions.disabled-child-contains-tabs',
'Cannot change to tabs because a row already contains tabs'
);
}
if (disableTabs && opt.id === TabsLayoutManager.descriptor.id) {
description = t('dashboard.canvas-actions.disabled-nested-tabs', 'Tabs cannot be nested inside other tabs');
disabledOptions.push(opt);
}

View File

@@ -1,93 +0,0 @@
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabItem } from '../layout-tabs/TabItem';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { containsTabsLayout, findAllGridTypes } from './findAllGridTypes';
describe('findAllGridTypes', () => {
it('should return grid type for a grid layout', () => {
const layout = AutoGridLayoutManager.createEmpty();
expect(findAllGridTypes(layout)).toEqual([AutoGridLayoutManager.descriptor.id]);
});
it('should return grid types from tabs', () => {
const layout = new TabsLayoutManager({
tabs: [
new TabItem({ layout: AutoGridLayoutManager.createEmpty() }),
new TabItem({ layout: AutoGridLayoutManager.createEmpty() }),
],
});
expect(findAllGridTypes(layout)).toEqual([
AutoGridLayoutManager.descriptor.id,
AutoGridLayoutManager.descriptor.id,
]);
});
it('should return grid types from rows', () => {
const layout = new RowsLayoutManager({
rows: [
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
],
});
expect(findAllGridTypes(layout)).toEqual([
AutoGridLayoutManager.descriptor.id,
AutoGridLayoutManager.descriptor.id,
]);
});
});
describe('containsTabsLayout', () => {
it('should return true when layout is TabsLayoutManager', () => {
const layout = new TabsLayoutManager({
tabs: [new TabItem({ layout: AutoGridLayoutManager.createEmpty() })],
});
expect(containsTabsLayout(layout)).toBe(true);
});
it('should return false when layout is a grid layout', () => {
const layout = AutoGridLayoutManager.createEmpty();
expect(containsTabsLayout(layout)).toBe(false);
});
it('should return false when layout is RowsLayoutManager with no tabs in rows', () => {
const layout = new RowsLayoutManager({
rows: [
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
],
});
expect(containsTabsLayout(layout)).toBe(false);
});
it('should return true when RowsLayoutManager contains a row with tabs layout', () => {
const layout = new RowsLayoutManager({
rows: [
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
new RowItem({
layout: new TabsLayoutManager({
tabs: [new TabItem({ layout: AutoGridLayoutManager.createEmpty() })],
}),
}),
],
});
expect(containsTabsLayout(layout)).toBe(true);
});
it('should return true when any row contains tabs layout', () => {
const layout = new RowsLayoutManager({
rows: [
new RowItem({
layout: new TabsLayoutManager({
tabs: [new TabItem({ layout: AutoGridLayoutManager.createEmpty() })],
}),
}),
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
new RowItem({ layout: AutoGridLayoutManager.createEmpty() }),
],
});
expect(containsTabsLayout(layout)).toBe(true);
});
});

View File

@@ -15,15 +15,3 @@ export function findAllGridTypes(layout: DashboardLayoutManager): string[] {
return [];
}
export function containsTabsLayout(layout: DashboardLayoutManager): boolean {
if (layout instanceof TabsLayoutManager) {
return true;
}
if (layout instanceof RowsLayoutManager) {
return layout.state.rows.some((row) => containsTabsLayout(row.getLayout()));
}
return false;
}

View File

@@ -18,10 +18,6 @@ export const validateDashboardJson = (json: string) => {
if (hasInvalidTag) {
return t('dashboard.validation.tags-expected-strings', 'tags expected array of strings');
}
const hasTooLongTag = dashboard.tags.some((tag: string) => tag.length > 50);
if (hasTooLongTag) {
return t('dashboard.validation.tag-too-long', 'Dashboard tag too long, max 50 characters');
}
} else {
return t('dashboard.validation.tags-expected-array', 'tags expected array');
}

View File

@@ -10808,6 +10808,18 @@
"help/documentation": "Dokumentace",
"help/keyboard-shortcuts": "Klávesové zkratky",
"help/support": "Podpora",
"history-container": {
"drawer-tittle": "Historie"
},
"history-wrapper": {
"collapse": "Sbalit",
"expand": "Rozbalit",
"icon-selected": "Vybraný záznam",
"icon-unselected": "Normální záznam",
"show-more": "Zobrazit více",
"today": "Dnes",
"yesterday": "Včera"
},
"home": {
"title": "Domů"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Dokumentation",
"help/keyboard-shortcuts": "Tastaturbefehle",
"help/support": "Support",
"history-container": {
"drawer-tittle": "Verlauf"
},
"history-wrapper": {
"collapse": "Einklappen",
"expand": "Ausklappen",
"icon-selected": "Ausgewählter Eintrag",
"icon-unselected": "Normaler Eintrag",
"show-more": "Mehr anzeigen",
"today": "Heute",
"yesterday": "Gestern"
},
"home": {
"title": "Home"
},

View File

@@ -4614,7 +4614,6 @@
},
"canvas-actions": {
"add-panel": "Add panel",
"disabled-child-contains-tabs": "Cannot change to tabs because a row already contains tabs",
"disabled-nested-grouping": "Grouping is limited to 2 levels",
"disabled-nested-tabs": "Tabs cannot be nested inside other tabs",
"group-into-row": "Group into row",
@@ -5696,7 +5695,6 @@
"validation": {
"invalid-dashboard-id": "Could not find a valid Grafana.com ID",
"invalid-json": "Not valid JSON",
"tag-too-long": "Dashboard tag too long, max 50 characters",
"tags-expected-array": "tags expected array",
"tags-expected-strings": "tags expected array of strings"
},
@@ -9255,8 +9253,7 @@
"tags-input": {
"add": "Add",
"placeholder-new-tag": "New tag (enter key to add)",
"remove": "Remove tag: {{name}}",
"tag-too-long": "Tag too long, max 50 characters"
"remove": "Remove tag: {{name}}"
},
"time-sync-button": {
"aria-label-sync": "Sync times",

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Documentación",
"help/keyboard-shortcuts": "Atajos de teclado",
"help/support": "Asistencia",
"history-container": {
"drawer-tittle": "Historial"
},
"history-wrapper": {
"collapse": "Contraer",
"expand": "Expandir",
"icon-selected": "Entrada seleccionada",
"icon-unselected": "Entrada normal",
"show-more": "Mostrar más",
"today": "Hoy",
"yesterday": "Ayer"
},
"home": {
"title": "Inicio"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Documentation",
"help/keyboard-shortcuts": "Raccourcis clavier",
"help/support": "Assistance",
"history-container": {
"drawer-tittle": "Historique"
},
"history-wrapper": {
"collapse": "Réduire",
"expand": "Développer",
"icon-selected": "Entrée sélectionnée",
"icon-unselected": "Entrée normale",
"show-more": "Afficher plus",
"today": "Aujourd'hui",
"yesterday": "Hier"
},
"home": {
"title": "Accueil"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Dokumentáció",
"help/keyboard-shortcuts": "Gyorsbillentyűk",
"help/support": "Ügyfélszolgálat",
"history-container": {
"drawer-tittle": "Előzmények"
},
"history-wrapper": {
"collapse": "Összecsukás",
"expand": "Kibontás",
"icon-selected": "Kijelölt bejegyzés",
"icon-unselected": "Normál bejegyzés",
"show-more": "Több megjelenítése",
"today": "Ma",
"yesterday": "Tegnap"
},
"home": {
"title": "Kezdőlap"
},

View File

@@ -10676,6 +10676,18 @@
"help/documentation": "Dokumentasi",
"help/keyboard-shortcuts": "Pintasan keyboard",
"help/support": "Dukungan",
"history-container": {
"drawer-tittle": "Sejarah"
},
"history-wrapper": {
"collapse": "Ciutkan",
"expand": "Perluas",
"icon-selected": "Entri yang dipilih",
"icon-unselected": "Entri Normal",
"show-more": "Tampilkan lebih banyak",
"today": "Hari ini",
"yesterday": "Kemarin"
},
"home": {
"title": "Beranda"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Documentazione",
"help/keyboard-shortcuts": "Scelte rapide da tastiera",
"help/support": "Servizio Clienti",
"history-container": {
"drawer-tittle": "Cronologia"
},
"history-wrapper": {
"collapse": "Riduci",
"expand": "Espandi",
"icon-selected": "Voce selezionata",
"icon-unselected": "Ingresso normale",
"show-more": "Mostra di più",
"today": "Oggi",
"yesterday": "Ieri"
},
"home": {
"title": "Home"
},

View File

@@ -10676,6 +10676,18 @@
"help/documentation": "ドキュメント",
"help/keyboard-shortcuts": "キーボードショートカット",
"help/support": "サポート",
"history-container": {
"drawer-tittle": "履歴"
},
"history-wrapper": {
"collapse": "折りたたみ表示",
"expand": "展開",
"icon-selected": "選択したエントリー",
"icon-unselected": "通常のエントリー",
"show-more": "さらに表示",
"today": "今日",
"yesterday": "昨日"
},
"home": {
"title": "ホーム"
},

View File

@@ -10676,6 +10676,18 @@
"help/documentation": "문서",
"help/keyboard-shortcuts": "키보드 단축키",
"help/support": "지원",
"history-container": {
"drawer-tittle": "이력"
},
"history-wrapper": {
"collapse": "접기",
"expand": "펼치기",
"icon-selected": "선택된 항목",
"icon-unselected": "일반 항목",
"show-more": "더 보기",
"today": "오늘",
"yesterday": "어제"
},
"home": {
"title": "홈"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Documentatie",
"help/keyboard-shortcuts": "Sneltoetsen",
"help/support": "Ondersteuning",
"history-container": {
"drawer-tittle": "Geschiedenis"
},
"history-wrapper": {
"collapse": "Samenvouwen",
"expand": "Uitvouwen",
"icon-selected": "Geselecteerde invoer",
"icon-unselected": "Gewone invoer",
"show-more": "Meer weergeven",
"today": "Vandaag",
"yesterday": "Gisteren"
},
"home": {
"title": "Startpagina"
},

View File

@@ -10808,6 +10808,18 @@
"help/documentation": "Dokumentacja",
"help/keyboard-shortcuts": "Skróty klawiaturowe",
"help/support": "Wsparcie",
"history-container": {
"drawer-tittle": "Historia"
},
"history-wrapper": {
"collapse": "Zwiń",
"expand": "Rozwiń",
"icon-selected": "Zaznaczony wpis",
"icon-unselected": "Normalny wpis",
"show-more": "Pokaż więcej",
"today": "Dzisiaj",
"yesterday": "Wczoraj"
},
"home": {
"title": "Strona główna"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Documentação",
"help/keyboard-shortcuts": "Atalhos do teclado",
"help/support": "Suporte",
"history-container": {
"drawer-tittle": "Histórico"
},
"history-wrapper": {
"collapse": "Recolher",
"expand": "Expandir",
"icon-selected": "Entrada selecionada",
"icon-unselected": "Entrada normal",
"show-more": "Exibir mais",
"today": "Hoje",
"yesterday": "Ontem"
},
"home": {
"title": "Página inicial"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Documentação",
"help/keyboard-shortcuts": "Atalhos de teclado",
"help/support": "Apoio",
"history-container": {
"drawer-tittle": "Histórico"
},
"history-wrapper": {
"collapse": "Recolher",
"expand": "Expandir",
"icon-selected": "Entrada selecionada",
"icon-unselected": "Entrada normal",
"show-more": "Mostrar mais",
"today": "Hoje",
"yesterday": "Ontem"
},
"home": {
"title": "Início"
},

View File

@@ -10808,6 +10808,18 @@
"help/documentation": "Документация",
"help/keyboard-shortcuts": "Сочетания клавиш",
"help/support": "Поддержка",
"history-container": {
"drawer-tittle": "История"
},
"history-wrapper": {
"collapse": "Свернуть",
"expand": "Развернуть",
"icon-selected": "Выделенная запись",
"icon-unselected": "Обычная запись",
"show-more": "Показать еще",
"today": "Сегодня",
"yesterday": "Вчера"
},
"home": {
"title": "Главная"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Dokumentation",
"help/keyboard-shortcuts": "Tangentbordsgenvägar",
"help/support": "Support",
"history-container": {
"drawer-tittle": "Historik"
},
"history-wrapper": {
"collapse": "Minimera",
"expand": "Expandera",
"icon-selected": "Markerad inmatning",
"icon-unselected": "Normal inmatning",
"show-more": "Visa mer",
"today": "Idag",
"yesterday": "Igår"
},
"home": {
"title": "Hem"
},

View File

@@ -10720,6 +10720,18 @@
"help/documentation": "Belgeler",
"help/keyboard-shortcuts": "Klavye kısayolları",
"help/support": "Destek",
"history-container": {
"drawer-tittle": "Geçmiş"
},
"history-wrapper": {
"collapse": "Daralt",
"expand": "Genişlet",
"icon-selected": "Seçili giriş",
"icon-unselected": "Normal Giriş",
"show-more": "Daha fazla göster",
"today": "Bugün",
"yesterday": "Dün"
},
"home": {
"title": "Ana sayfa"
},

View File

@@ -10676,6 +10676,18 @@
"help/documentation": "文档",
"help/keyboard-shortcuts": "快捷键",
"help/support": "支持",
"history-container": {
"drawer-tittle": "历史记录"
},
"history-wrapper": {
"collapse": "收起",
"expand": "展开",
"icon-selected": "所选条目",
"icon-unselected": "正常条目",
"show-more": "显示更多",
"today": "今天",
"yesterday": "昨天"
},
"home": {
"title": "首页"
},

Some files were not shown because too many files have changed in this diff Show More