Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion pipelines/types/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -725,10 +725,28 @@ func (s *GrafanaDashboardsStep) IsWellFormedOverInputs() bool {

const StepActionGrafanaDatasources = "GrafanaDatasources"

type GrafanaAzureMonitorDatasources struct {
Enabled *bool `json:"enabled,omitempty"`
}

type GrafanaADXDatasource struct {
Enabled Value `json:"enabled,omitempty"`
Comment thread
swiencki marked this conversation as resolved.
DeleteWhenDisabled bool `json:"deleteWhenDisabled,omitempty"`
ClusterURL Value `json:"clusterUrl,omitempty"`
DefaultDatabase Value `json:"defaultDatabase,omitempty"`
DatasourceName Value `json:"datasourceName,omitempty"`
Geographies Value `json:"geographies,omitempty"`
DataConsistency string `json:"dataConsistency,omitempty"`
}

type GrafanaDatasourcesStep struct {
StepMeta `json:",inline"`

GrafanaName string `json:"grafanaName"`
GrafanaName string `json:"grafanaName"`
GrafanaResourceID *Value `json:"grafanaResourceId,omitempty"`

AzureMonitor *GrafanaAzureMonitorDatasources `json:"azureMonitor,omitempty"`
ADX *GrafanaADXDatasource `json:"adx,omitempty"`

// SkipSync indicates whether to skip syncing datasources. It is intended for prow jobs to skip syncing datasources.
SkipSync bool `json:"skipSync,omitempty"`
Comment thread
swiencki marked this conversation as resolved.
Expand All @@ -746,6 +764,16 @@ func (s *GrafanaDatasourcesStep) RequiredInputs() []StepDependency {
for _, val := range []Input{s.IdentityFrom} {
deps = append(deps, val.StepDependency)
}
if s.GrafanaResourceID != nil && s.GrafanaResourceID.Input != nil {
deps = append(deps, s.GrafanaResourceID.Input.StepDependency)
}
if s.ADX != nil {
for _, val := range []Value{s.ADX.Enabled, s.ADX.ClusterURL, s.ADX.DefaultDatabase, s.ADX.DatasourceName, s.ADX.Geographies} {
if val.Input != nil {
deps = append(deps, val.Input.StepDependency)
}
}
}

slices.SortFunc(deps, SortDependencies)
deps = slices.Compact(deps)
Expand Down
22 changes: 22 additions & 0 deletions pipelines/types/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,28 @@ func TestRequiredInputs(t *testing.T) {
name: "pav2 empty",
input: &Pav2Step{},
},
{
name: "grafana datasources full",
input: &GrafanaDatasourcesStep{
IdentityFrom: Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "identity"}},
GrafanaResourceID: &Value{Input: &Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "global"}}},
ADX: &GrafanaADXDatasource{
Enabled: Value{Input: &Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "config"}}},
ClusterURL: Value{Input: &Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "kusto"}}},
DefaultDatabase: Value{Input: &Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "config"}}},
DatasourceName: Value{Input: &Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "name"}}},
Geographies: Value{Input: &Input{StepDependency: StepDependency{ResourceGroup: "rg", Step: "geo"}}},
},
},
expected: []StepDependency{
{ResourceGroup: "rg", Step: "config"},
{ResourceGroup: "rg", Step: "geo"},
{ResourceGroup: "rg", Step: "global"},
{ResourceGroup: "rg", Step: "identity"},
{ResourceGroup: "rg", Step: "kusto"},
{ResourceGroup: "rg", Step: "name"},
},
},
} {
t.Run(testCase.name, func(t *testing.T) {
if diff := cmp.Diff(testCase.expected, testCase.input.RequiredInputs()); diff != "" {
Expand Down
47 changes: 46 additions & 1 deletion pipelines/types/pipeline.schema.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,42 @@
}
]
},
"grafanaAzureMonitorDatasources": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"grafanaADXDatasource": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"$ref": "#/definitions/value"
},
"deleteWhenDisabled": {
"type": "boolean"
},
"clusterUrl": {
"$ref": "#/definitions/value"
},
"defaultDatabase": {
"$ref": "#/definitions/value"
},
"datasourceName": {
"$ref": "#/definitions/value"
},
"geographies": {
"$ref": "#/definitions/value"
},
"dataConsistency": {
"type": "string"
}
}
},
"grafanaDatasourcesStep": {
"unevaluatedProperties": false,
"allOf": [
Expand All @@ -702,8 +738,17 @@
"grafanaName": {
"type": "string"
},
"grafanaResourceId": {
"$ref": "#/definitions/value"
},
"azureMonitor": {
"$ref": "#/definitions/grafanaAzureMonitorDatasources"
},
"adx": {
"$ref": "#/definitions/grafanaADXDatasource"
},
"skipSync": {
"type": "string"
"type": "boolean"
},
"identityFrom": {
"$ref": "#/definitions/input"
Expand Down
59 changes: 59 additions & 0 deletions pipelines/types/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,65 @@ func TestValidatePipelineSchema(t *testing.T) {
},
},
},
{
name: "valid grafana datasources with adx",
pipeline: map[string]interface{}{
"serviceGroup": "test",
"rolloutName": "test",
"resourceGroups": []interface{}{
map[string]interface{}{
"name": "rg",
"resourceGroup": "rg",
"subscription": "sub",
"steps": []interface{}{
map[string]interface{}{
"name": "datasources",
"action": "GrafanaDatasources",
"grafanaName": "grafana",
"grafanaResourceId": map[string]interface{}{
"input": map[string]interface{}{
"resourceGroup": "global",
"step": "output",
"name": "grafanaResourceId",
},
},
"identityFrom": map[string]interface{}{
"resourceGroup": "rg",
"step": "deploy",
"name": "msi",
},
"azureMonitor": map[string]interface{}{
"enabled": false,
},
"adx": map[string]interface{}{
"enabled": map[string]interface{}{
"configRef": "monitoring.adxDatasourceEnabled",
},
"deleteWhenDisabled": true,
"clusterUrl": map[string]interface{}{
"input": map[string]interface{}{
"resourceGroup": "rg",
"step": "kusto",
"name": "kustoUri",
},
},
"defaultDatabase": map[string]interface{}{
"value": "ServiceLogs",
},
"datasourceName": map[string]interface{}{
"value": "kusto-int-uksouth",
},
"geographies": map[string]interface{}{
"configRef": "monitoring.adxDatasourceGeographies",
},
"dataConsistency": "strongconsistency",
},
},
},
},
},
},
},
{
name: "invalid",
pipeline: map[string]interface{}{
Expand Down
48 changes: 47 additions & 1 deletion tools/grafanactl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ grafanactl helps maintain Azure Managed Grafana instances by providing tools to:
- List all datasources in a Grafana instance
- Remove orphaned Azure Monitor Workspace integrations
- Clean up stale datasources pointing to deleted resources
- Reconcile Azure Data Explorer datasources
- Sync dashboards and folders from git to Grafana

This tool is particularly useful when Azure Monitor Workspaces (Prometheus instances) are removed from your infrastructure but their references remain in Grafana, creating stale integrations.
Expand Down Expand Up @@ -39,6 +40,7 @@ All commands require these basic parameters:
- `--subscription` - Azure subscription ID
- `--resource-group` - Azure resource group name
- `--grafana-name` - Azure Managed Grafana instance name
- `--grafana-resource-id` - Azure Managed Grafana resource ID, as an alternative to subscription/resource group/name
- `--output` - Output format: `table` (default) or `json`
- `-v, --verbosity` - Set logging verbosity level (0-10)

Expand Down Expand Up @@ -110,6 +112,51 @@ grafanactl clean fixup-datasources \
--grafana-name "your-grafana-instance"
```

### Modify Commands

Modify commands reconcile resources in Azure Managed Grafana.

#### Reconcile Datasources

Reconcile Azure Monitor Workspace integrations and, when enabled, one Azure Data
Explorer datasource using Grafana's REST API:

```bash
# Preview changes (dry-run)
grafanactl modify datasource reconcile \
--grafana-resource-id "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Dashboard/grafana/<grafana-name>" \
--azure-monitor-enabled=false \
--adx-enabled=true \
--adx-delete-when-disabled=true \
--adx-cluster-url "https://example.region.kusto.windows.net" \
--adx-default-database "ServiceLogs" \
--adx-geographies "uksouth,eastus2" \
--adx-current-geography "uksouth" \
--adx-datasource-name "kusto-int-uksouth" \
--dry-run

# Apply changes
grafanactl modify datasource reconcile \
--grafana-resource-id "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Dashboard/grafana/<grafana-name>" \
--azure-monitor-enabled=false \
--adx-enabled=true \
--adx-delete-when-disabled=true \
--adx-cluster-url "https://example.region.kusto.windows.net" \
--adx-default-database "ServiceLogs" \
--adx-geographies "uksouth,eastus2" \
--adx-current-geography "uksouth" \
--adx-datasource-name "kusto-int-uksouth"
```
Comment thread
swiencki marked this conversation as resolved.

When `--adx-enabled=false --adx-delete-when-disabled=true` are both set, the
command deletes the named ADX datasource if it exists. ADX create/update requires
the Azure Data Explorer datasource plugin
(`grafana-azure-data-explorer-datasource`) to be available in Grafana. The
datasource uses the Grafana managed identity for ADX authentication and fails if
an existing datasource with the requested name has a different plugin type. When
`--adx-geographies` is set, the command validates the comma-separated geography
allowlist and disables ADX desired state for geographies not in the list.

### Sync Commands

Sync commands help keep your Grafana instance in sync with dashboard definitions stored in git.
Expand Down Expand Up @@ -148,4 +195,3 @@ The config file (e.g., `observability.yaml`) defines:
- The tool includes retry logic for transient Azure API failures
- Use `--verbosity` flag to increase logging detail for troubleshooting
- Always use `--dry-run` first to preview changes before applying them

Loading
Loading