Compare commits
2 Commits
eledobleef
...
gamab/auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f71c8f562 | ||
|
|
d7a3d61726 |
@@ -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
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -197,7 +197,6 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
|
||||
&HistoricJobList{},
|
||||
&Connection{},
|
||||
&ConnectionList{},
|
||||
&ExternalRepositoryList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]`
|
||||
|
||||
|
||||
@@ -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
10
go.mod
@@ -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
15
go.sum
@@ -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=
|
||||
|
||||
1
go.work
1
go.work
@@ -32,7 +32,6 @@ use (
|
||||
./pkg/build
|
||||
./pkg/build/wire // skip:golangci-lint
|
||||
./pkg/codegen
|
||||
./pkg/plugins
|
||||
./pkg/plugins/codegen
|
||||
./pkg/promlib
|
||||
./pkg/semconv
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
14
public/api-enterprise-spec.json
generated
14
public/api-enterprise-spec.json
generated
@@ -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
14
public/api-merged.json
generated
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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ů"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": "ホーム"
|
||||
},
|
||||
|
||||
@@ -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": "홈"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": "Главная"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user