Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI: Paths to rules provisioning are incorrect in api-merged.json #76578

Open
mbarrien opened this issue Oct 13, 2023 · 5 comments
Open

OpenAPI: Paths to rules provisioning are incorrect in api-merged.json #76578

mbarrien opened this issue Oct 13, 2023 · 5 comments
Assignees
Labels
area/alerting Grafana Alerting

Comments

@mbarrien
Copy link

mbarrien commented Oct 13, 2023

What happened?

In the public api-merged.json file the path to all the rules provisioning APIs are incorrect.

grafana/public/api-merged.json

Lines 2468 to 3372 in 889576a

"/api/v1/provisioning/alert-rules": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get all the alert rules.",
"operationId": "RouteGetAlertRules",
"responses": {
"200": {
"description": "ProvisionedAlertRules",
"schema": {
"$ref": "#/definitions/ProvisionedAlertRules"
}
}
}
},
"post": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Create a new alert rule.",
"operationId": "RoutePostAlertRule",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/ProvisionedAlertRule"
}
},
{
"type": "string",
"name": "X-Disable-Provenance",
"in": "header"
}
],
"responses": {
"201": {
"description": "ProvisionedAlertRule",
"schema": {
"$ref": "#/definitions/ProvisionedAlertRule"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
},
"/api/v1/provisioning/alert-rules/export": {
"get": {
"tags": [
"provisioning"
],
"summary": "Export all alert rules in provisioning file format.",
"operationId": "RouteGetAlertRulesExport",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
},
{
"type": "string",
"default": "yaml",
"description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.",
"name": "format",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"description": "UIDs of folders from which to export rules",
"name": "folderUid",
"in": "query"
},
{
"type": "string",
"description": "Name of group of rules to export. Must be specified only together with a single folder UID",
"name": "group",
"in": "query"
},
{
"type": "string",
"description": "UID of alert rule to export. If specified, parameters folderUid and group must be empty.",
"name": "ruleUid",
"in": "query"
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/alert-rules/{UID}": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get a specific alert rule by UID.",
"operationId": "RouteGetAlertRule",
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "ProvisionedAlertRule",
"schema": {
"$ref": "#/definitions/ProvisionedAlertRule"
}
},
"404": {
"description": " Not found."
}
}
},
"put": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Update an existing alert rule.",
"operationId": "RoutePutAlertRule",
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/ProvisionedAlertRule"
}
},
{
"type": "string",
"name": "X-Disable-Provenance",
"in": "header"
}
],
"responses": {
"200": {
"description": "ProvisionedAlertRule",
"schema": {
"$ref": "#/definitions/ProvisionedAlertRule"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"tags": [
"provisioning"
],
"summary": "Delete a specific alert rule by UID.",
"operationId": "RouteDeleteAlertRule",
"parameters": [
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": " The alert rule was deleted successfully."
}
}
}
},
"/api/v1/provisioning/alert-rules/{UID}/export": {
"get": {
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"tags": [
"provisioning"
],
"summary": "Export an alert rule in provisioning file format.",
"operationId": "RouteGetAlertRuleExport",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
},
{
"type": "string",
"default": "yaml",
"description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.",
"name": "format",
"in": "query"
},
{
"type": "string",
"description": "Alert rule UID",
"name": "UID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/contact-points": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get all the contact points.",
"operationId": "RouteGetContactpoints",
"parameters": [
{
"type": "string",
"description": "Filter by name",
"name": "name",
"in": "query"
}
],
"responses": {
"200": {
"description": "ContactPoints",
"schema": {
"$ref": "#/definitions/ContactPoints"
}
}
}
},
"post": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Create a contact point.",
"operationId": "RoutePostContactpoints",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/EmbeddedContactPoint"
}
}
],
"responses": {
"202": {
"description": "EmbeddedContactPoint",
"schema": {
"$ref": "#/definitions/EmbeddedContactPoint"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
},
"/api/v1/provisioning/contact-points/export": {
"get": {
"tags": [
"provisioning"
],
"summary": "Export all contact points in provisioning file format.",
"operationId": "RouteGetContactpointsExport",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
},
{
"type": "string",
"default": "yaml",
"description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.",
"name": "format",
"in": "query"
},
{
"type": "boolean",
"default": false,
"description": "Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings.",
"name": "decrypt",
"in": "query"
},
{
"type": "string",
"description": "Filter by name",
"name": "name",
"in": "query"
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"403": {
"description": "PermissionDenied",
"schema": {
"$ref": "#/definitions/PermissionDenied"
}
}
}
}
},
"/api/v1/provisioning/contact-points/{UID}": {
"put": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Update an existing contact point.",
"operationId": "RoutePutContactpoint",
"parameters": [
{
"type": "string",
"description": "UID is the contact point unique identifier",
"name": "UID",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/EmbeddedContactPoint"
}
}
],
"responses": {
"202": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Delete a contact point.",
"operationId": "RouteDeleteContactpoints",
"parameters": [
{
"type": "string",
"description": "UID is the contact point unique identifier",
"name": "UID",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": " The contact point was deleted successfully."
}
}
}
},
"/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get a rule group.",
"operationId": "RouteGetAlertRuleGroup",
"parameters": [
{
"type": "string",
"name": "FolderUID",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "AlertRuleGroup",
"schema": {
"$ref": "#/definitions/AlertRuleGroup"
}
},
"404": {
"description": " Not found."
}
}
},
"put": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Update the interval of a rule group.",
"operationId": "RoutePutAlertRuleGroup",
"parameters": [
{
"type": "string",
"name": "FolderUID",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/AlertRuleGroup"
}
}
],
"responses": {
"200": {
"description": "AlertRuleGroup",
"schema": {
"$ref": "#/definitions/AlertRuleGroup"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
},
"/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": {
"get": {
"produces": [
"application/json",
"application/yaml",
"text/yaml"
],
"tags": [
"provisioning"
],
"summary": "Export an alert rule group in provisioning file format.",
"operationId": "RouteGetAlertRuleGroupExport",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "Whether to initiate a download of the file or not.",
"name": "download",
"in": "query"
},
{
"type": "string",
"default": "yaml",
"description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.",
"name": "format",
"in": "query"
},
{
"type": "string",
"name": "FolderUID",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/mute-timings": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get all the mute timings.",
"operationId": "RouteGetMuteTimings",
"responses": {
"200": {
"description": "MuteTimings",
"schema": {
"$ref": "#/definitions/MuteTimings"
}
}
}
},
"post": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Create a new mute timing.",
"operationId": "RoutePostMuteTiming",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/MuteTimeInterval"
}
}
],
"responses": {
"201": {
"description": "MuteTimeInterval",
"schema": {
"$ref": "#/definitions/MuteTimeInterval"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
}
},
"/api/v1/provisioning/mute-timings/{name}": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get a mute timing.",
"operationId": "RouteGetMuteTiming",
"parameters": [
{
"type": "string",
"description": "Mute timing name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "MuteTimeInterval",
"schema": {
"$ref": "#/definitions/MuteTimeInterval"
}
},
"404": {
"description": " Not found."
}
}
},
"put": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Replace an existing mute timing.",
"operationId": "RoutePutMuteTiming",
"parameters": [
{
"type": "string",
"description": "Mute timing name",
"name": "name",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/MuteTimeInterval"
}
}
],
"responses": {
"200": {
"description": "MuteTimeInterval",
"schema": {
"$ref": "#/definitions/MuteTimeInterval"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"tags": [
"provisioning"
],
"summary": "Delete a mute timing.",
"operationId": "RouteDeleteMuteTiming",
"parameters": [
{
"type": "string",
"description": "Mute timing name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": " The mute timing was deleted successfully."
}
}
}
},
"/api/v1/provisioning/policies": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get the notification policy tree.",
"operationId": "RouteGetPolicyTree",
"responses": {
"200": {
"description": "Route",
"schema": {
"$ref": "#/definitions/Route"
}
}
}
},
"put": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Sets the notification policy tree.",
"operationId": "RoutePutPolicyTree",
"parameters": [
{
"description": "The new notification routing tree to use",
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/Route"
}
}
],
"responses": {
"202": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Clears the notification policy tree.",
"operationId": "RouteResetPolicyTree",
"responses": {
"202": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
}
}
}
},
"/api/v1/provisioning/policies/export": {
"get": {
"tags": [
"provisioning"
],
"summary": "Export the notification policy tree in provisioning file format.",
"operationId": "RouteGetPolicyTreeExport",
"responses": {
"200": {
"description": "AlertingFileExport",
"schema": {
"$ref": "#/definitions/AlertingFileExport"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
}
},
"/api/v1/provisioning/templates": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get all notification templates.",
"operationId": "RouteGetTemplates",
"responses": {
"200": {
"description": "NotificationTemplates",
"schema": {
"$ref": "#/definitions/NotificationTemplates"
}
},
"404": {
"description": " Not found."
}
}
}
},
"/api/v1/provisioning/templates/{name}": {
"get": {
"tags": [
"provisioning"
],
"summary": "Get a notification template.",
"operationId": "RouteGetTemplate",
"parameters": [
{
"type": "string",
"description": "Template Name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "NotificationTemplate",
"schema": {
"$ref": "#/definitions/NotificationTemplate"
}
},
"404": {
"description": " Not found."
}
}
},
"put": {
"consumes": [
"application/json"
],
"tags": [
"provisioning"
],
"summary": "Updates an existing notification template.",
"operationId": "RoutePutTemplate",
"parameters": [
{
"type": "string",
"description": "Template Name",
"name": "name",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/NotificationTemplateContent"
}
}
],
"responses": {
"202": {
"description": "NotificationTemplate",
"schema": {
"$ref": "#/definitions/NotificationTemplate"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"tags": [
"provisioning"
],
"summary": "Delete a template.",
"operationId": "RouteDeleteTemplate",
"parameters": [
{
"type": "string",
"description": "Template Name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": " The template was deleted successfully."
}
}
}
},

(the commit pointed to is the current HEAD of main) shows several paths that start with "/api/v1/provisioning".

However the openapi file already has the following basePath:

"basePath": "/api",

So it already prepends /api to all paths. In combination this ends up with a paths like /api/api/v1/provisioning/alert-rules in the generated code (note the double /api), which 404s. All other paths in the file are missing the "/api" prefix.

This is evident when using Golang generated apis from this OpenAPI file. The self-contained example I've included below uses go-swagger v0.30.5, the same library used in https://github.com/grafana/grafana-openapi-client-go (as tracked in #47287)

What did you expect to happen?

We are correctly able to access the resources in the API.

Did this work before?

Not sure. Haven't tried to use OpenAPI with earlier versions of Grafana.

How do we reproduce it?

This is a minimized, self-contained reproduction that originally comes from code inspired from https://github.com/esnet/grafana-swagger-api-golang

`

go mod init example.com/grafana-openapi-bug
go get github.com/go-swagger/[email protected]
go install github.com/go-swagger/go-swagger/cmd/[email protected]
# Using most recent tagged Grafana version v10.1.5
curl -o api-merged.json https://raw.githubusercontent.com/grafana/grafana/v10.1.5/public/api-merged.json
mkdir -p goclient
$(go env GOPATH)/bin/swagger generate client -f api-merged.json -t ./goclient --skip-validation --with-flatten=remove-unused --additional-initialism=DTO,API,OK,LDAP,ACL,SNS,CSV --keep-spec-order

Save the following to main.go:

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/url"

	gclient "example.com/grafana-openapi-bug/goclient/client"
	"example.com/grafana-openapi-bug/goclient/client/provisioning"
	"github.com/go-openapi/runtime"
	"github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
)

type APIKeyAuthenticator struct {
	APIKey string
}

func (a APIKeyAuthenticator) AuthenticateRequest(req runtime.ClientRequest, reg strfmt.Registry) error {
	return req.SetHeaderParam("Authorization", fmt.Sprintf("Bearer %s", a.APIKey))
}

func main() {
	parsedUrl, _ := url.Parse("https://INSERT_URL_HERE/")
	httpClient := &http.Client{}
	runtimeClient := client.NewWithClient(parsedUrl.Host, gclient.DefaultBasePath, []string{parsedUrl.Scheme}, httpClient)
	grafanaClient := gclient.New(runtimeClient, nil)
	token := "INSERT_TOKEN_HERE"
	authInfo := APIKeyAuthenticator{
		APIKey: token,
	}
	uid := "YOUR_ALERT_UID"
	params := provisioning.NewRouteGetAlertRuleParams().WithUID(uid)
	alertruleOk, err := grafanaClient.Provisioning.RouteGetAlertRule(params, authInfo)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", alertruleOk)
}

will result in the following error message:

$ go get ./...
# ... Some output about libraries downloaded will be here ...
$ go build .
$ SWAGGER_DEBUG=1 ./grafana-openapi-bug
GET /api/api/v1/provisioning/alert-rules/YOUR_ALERT_UID HTTP/1.1
Host: INSERT_URL_HERE
User-Agent: Go-http-client/1.1
Accept: application/json
Authorization: Bearer INSERT_TOKEN_HERE
Accept-Encoding: gzip


HTTP/2.0 404 Not Found
...
{"message":"Not found"}

2023/10/13 12:54:34 [GET /api/v1/provisioning/alert-rules/{UID}][404] routeGetAlertRuleNotFound...

Notice in the first line the GET /api/api/v1...

The code work as expected if you run the following

# Replaces all the "api/v1" with just "v1", fixing this bug.
sed -i '' -e 's/api\/v1/v1/g' api-merged.json
# This fixes a 2nd bug in the provisioning OpenAPI: The `for` field returns a string formatted like `5m`, not an int64 like a Duration
cat <<'EOT' | patch -u api-merged.json
--- api-merged.original.json    2023-10-15 17:22:48
+++ api-merged.json     2023-10-15 17:31:58
@@ -16324,3 +16324,3 @@
         "for": {
-          "$ref": "#/definitions/Duration"
+          "type": "string"
         },
EOT
# Same swagger generate and go build lines as above
$(go env GOPATH)/bin/swagger generate client -f api-merged.json -t ./goclient --skip-validation --with-flatten=remove-unused --additional-initialism=DTO,API,OK,LDAP,ACL,SNS,CSV --keep-spec-order
go build .

Is the bug inside a dashboard panel?

No response

Environment (with versions)?

No response

Grafana platform?

I use Grafana Cloud

Datasource(s)?

No response

@mbarrien mbarrien changed the title OpenAPI: Paths to rules provisioning are incorrect in openapi3.json OpenAPI: Paths to rules provisioning are incorrect in api-merged.json Oct 14, 2023
@mbarrien
Copy link
Author

Note: this is apparently a duplicate of #76386 but this one has a lot more detail on reproducing the issue.

@safaci2000
Copy link

I opened up this ticket a few days ago, I wanted to call it out as this seems very much related. #76386

@nikimanoledaki
Copy link
Contributor

nikimanoledaki commented Oct 17, 2023

Thank you both @safaci2000 @mbarrien for finding this!

I can confirm that 17 endpoints in the OAPI spec have a path with /api prepended, which is inconsistent with other endpoints in the spec. Specifically, these are all /api/v1/provisioning paths related to provisioning alert rules. The spec for the Alerting APIs is generated separately in the Makefile: https://github.com/grafana/grafana/blob/main/Makefile#L44

This is an issue because we initialise the OAPI generated client to point at /api for all the other endpoints. For example, in the TF Provider, we use /api as the base path.

We need to update the swagger annotation for these 17 paths to not prepend /api to them.

@armandgrillet
Copy link
Contributor

Thank you for the detailed report, we will investigate what can be done to fix this. Previous trials merging our APIs have not been successful due to various issues.

@usmangt usmangt added the area/alerting Grafana Alerting label Oct 19, 2023
@rwwiv rwwiv moved this to Inbox in Alerting Oct 24, 2023
@armandgrillet armandgrillet moved this from Inbox to In progress in Alerting Nov 8, 2023
@julienduchesne
Copy link
Member

Would be fixed by #79025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/alerting Grafana Alerting
Projects
Status: In progress
Development

Successfully merging a pull request may close this issue.

7 participants