API: Extract OpenAPI specification from source code using go-swagger (#40528) (#45061)

* API: Using go-swagger for extracting OpenAPI specification from source code

* Merge Grafana Alerting spec

* Include enterprise endpoints (if enabled)

* Serve SwaggerUI under feature flag

* Fix building dev docker images

* Configure swaggerUI

* Add missing json tags

Co-authored-by: Ying WANG <ying.wang@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
(cherry picked from commit 35fe58de37)
This commit is contained in:
Grot (@grafanabot)
2022-02-08 13:52:05 +01:00
committed by GitHub
parent c6a58003d1
commit 08ad99c36e
61 changed files with 35314 additions and 74 deletions
+5
View File
@@ -110,6 +110,11 @@ var (
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "swaggerUi",
Description: "Serves swagger UI",
State: FeatureStateBeta,
},
{
Name: "featureHighlights",
Description: "Highlight Enterprise features",
+4
View File
@@ -83,6 +83,10 @@ const (
// only execute the query saved in a panel
FlagValidatedQueries = "validatedQueries"
// FlagSwaggerUi
// Serves swagger UI
FlagSwaggerUi = "swaggerUi"
// FlagFeatureHighlights
// Highlight Enterprise features
FlagFeatureHighlights = "featureHighlights"
+1 -1
View File
@@ -83,7 +83,7 @@ func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Res
// patchHandler handles PATCH /api/library-elements/:uid
func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Response {
cmd := patchLibraryElementCommand{}
cmd := PatchLibraryElementCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
+1 -1
View File
@@ -436,7 +436,7 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme
}
// patchLibraryElement updates a Library Element.
func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInUser *models.SignedInUser, cmd patchLibraryElementCommand, uid string) (LibraryElementDTO, error) {
func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInUser *models.SignedInUser, cmd PatchLibraryElementCommand, uid string) (LibraryElementDTO, error) {
var dto LibraryElementDTO
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
return LibraryElementDTO{}, err
@@ -14,7 +14,7 @@ import (
func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{Kind: int64(models.PanelElement), Version: 1}
cmd := PatchLibraryElementCommand{Kind: int64(models.PanelElement), Version: 1}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": "unknown"})
sc.reqContext.Req.Body = mockRequestBody(cmd)
resp := sc.service.patchHandler(sc.reqContext)
@@ -24,7 +24,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: newFolder.Id,
Name: "Panel - New name",
Model: []byte(`
@@ -87,7 +87,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: newFolder.Id,
Kind: int64(models.PanelElement),
Version: 1,
@@ -109,7 +109,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with name only, it should change name successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
Name: "New Name",
Kind: int64(models.PanelElement),
@@ -132,7 +132,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with a nonexistent UID, it should change UID successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
UID: util.GenerateShortUID(),
Kind: int64(models.PanelElement),
@@ -155,7 +155,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with an invalid UID, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
UID: "Testing an invalid UID",
Kind: int64(models.PanelElement),
@@ -169,7 +169,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with an UID that is too long, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
UID: "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K",
Kind: int64(models.PanelElement),
@@ -188,7 +188,7 @@ func TestPatchLibraryElement(t *testing.T) {
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
UID: command.UID,
Kind: int64(models.PanelElement),
@@ -202,7 +202,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with model only, it should change model successfully, sync type and description fields and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`),
Kind: int64(models.PanelElement),
@@ -231,7 +231,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with model.description only, it should change model successfully, sync type and description fields and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
Model: []byte(`{ "description": "New description" }`),
Kind: int64(models.PanelElement),
@@ -258,7 +258,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with model.type only, it should change model successfully, sync type and description fields and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: -1,
Model: []byte(`{ "type": "graph" }`),
Kind: int64(models.PanelElement),
@@ -285,7 +285,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(models.PanelElement)}
cmd := PatchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.UserId = 2
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
sc.ctx.Req.Body = mockRequestBody(cmd)
@@ -307,7 +307,7 @@ func TestPatchLibraryElement(t *testing.T) {
sc.ctx.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
Name: "Text - Library Panel",
Version: 1,
Kind: int64(models.PanelElement),
@@ -325,7 +325,7 @@ func TestPatchLibraryElement(t *testing.T) {
sc.ctx.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: 1,
Version: 1,
Kind: int64(models.PanelElement),
@@ -338,7 +338,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: sc.folder.Id,
Version: 1,
Kind: int64(models.PanelElement),
@@ -352,7 +352,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: sc.folder.Id,
Version: 1,
Kind: int64(models.PanelElement),
@@ -368,7 +368,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with an other kind, it should succeed but panel should not change",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{
cmd := PatchLibraryElementCommand{
FolderID: sc.folder.Id,
Version: 1,
Kind: int64(models.VariableElement),
@@ -86,7 +86,7 @@ func TestLibraryElementPermissions(t *testing.T) {
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(models.PanelElement)}
cmd := PatchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(models.PanelElement)}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID})
sc.reqContext.Req.Body = mockRequestBody(cmd)
resp = sc.service.patchHandler(sc.reqContext)
@@ -103,7 +103,7 @@ func TestLibraryElementPermissions(t *testing.T) {
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(models.PanelElement)}
cmd := PatchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(models.PanelElement)}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID})
sc.reqContext.Req.Body = mockRequestBody(cmd)
resp = sc.service.patchHandler(sc.reqContext)
@@ -154,7 +154,7 @@ func TestLibraryElementPermissions(t *testing.T) {
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(models.PanelElement)}
cmd := PatchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(models.PanelElement)}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID})
sc.ctx.Req.Body = mockRequestBody(cmd)
resp = sc.service.patchHandler(sc.reqContext)
@@ -170,7 +170,7 @@ func TestLibraryElementPermissions(t *testing.T) {
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(models.PanelElement)}
cmd := PatchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(models.PanelElement)}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID})
sc.ctx.Req.Body = mockRequestBody(cmd)
resp = sc.service.patchHandler(sc.reqContext)
@@ -219,7 +219,7 @@ func TestLibraryElementPermissions(t *testing.T) {
result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(models.PanelElement)}
cmd := PatchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(models.PanelElement)}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID})
sc.reqContext.Req.Body = mockRequestBody(cmd)
resp = sc.service.patchHandler(sc.reqContext)
+34 -13
View File
@@ -158,22 +158,43 @@ var (
// Commands
// CreateLibraryElementCommand is the command for adding a LibraryElement
// swagger:model
type CreateLibraryElementCommand struct {
FolderID int64 `json:"folderId"`
Name string `json:"name"`
Model json.RawMessage `json:"model"`
Kind int64 `json:"kind" binding:"Required"`
UID string `json:"uid"`
// ID of the folder where the library element is stored.
FolderID int64 `json:"folderId"`
// Name of the library element.
Name string `json:"name"`
// The JSON model for the library element.
// swagger:type object
Model json.RawMessage `json:"model"`
// Kind of element to create, Use 1 for library panels or 2 for c.
// Description:
// * 1 - library panels
// * 2 - library variables
// Enum: 1,2
Kind int64 `json:"kind" binding:"Required"`
// required: false
UID string `json:"uid"`
}
// patchLibraryElementCommand is the command for patching a LibraryElement
type patchLibraryElementCommand struct {
FolderID int64 `json:"folderId" binding:"Default(-1)"`
Name string `json:"name"`
Model json.RawMessage `json:"model,omitempty"`
Kind int64 `json:"kind" binding:"Required"`
Version int64 `json:"version" binding:"Required"`
UID string `json:"uid"`
// PatchLibraryElementCommand is the command for patching a LibraryElement
type PatchLibraryElementCommand struct {
// ID of the folder where the library element is stored.
FolderID int64 `json:"folderId" binding:"Default(-1)"`
// Name of the library element.
Name string `json:"name"`
// The JSON model for the library element.
Model json.RawMessage `json:"model,omitempty"`
// Kind of element to create, Use 1 for library panels or 2 for c.
// Description:
// * 1 - library panels
// * 2 - library variables
// Enum: 1,2
Kind int64 `json:"kind" binding:"Required"`
// Version of the library element you are updating.
Version int64 `json:"version" binding:"Required"`
// required: false
UID string `json:"uid"`
}
// searchLibraryElementsQuery is the query used for searching for Elements
+114 -4
View File
@@ -341,6 +341,108 @@
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"CreateDashboardSnapshotCommand": {
"properties": {
"Result": {
"$ref": "#/definitions/DashboardSnapshot"
},
"dashboard": {
"$ref": "#/definitions/Json"
},
"deleteKey": {
"description": "Unique key used to delete the snapshot. It is different from the `key` so that only the creator can delete the snapshot. Required if `external` is `true`.",
"type": "string",
"x-go-name": "DeleteKey"
},
"expires": {
"default": 0,
"description": "When the snapshot should expire in seconds in seconds. Default is never to expire.",
"format": "int64",
"type": "integer",
"x-go-name": "Expires"
},
"external": {
"default": false,
"description": "these are passed when storing an external snapshot ref\nSave the snapshot on an external server rather than locally.",
"type": "boolean",
"x-go-name": "External"
},
"key": {
"description": "Define the unique key. Required if `external` is `true`.",
"type": "string",
"x-go-name": "Key"
},
"name": {
"description": "Snapshot name",
"type": "string",
"x-go-name": "Name"
}
},
"required": [
"dashboard"
],
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"DashboardSnapshot": {
"description": "DashboardSnapshot model",
"properties": {
"Created": {
"format": "date-time",
"type": "string"
},
"Dashboard": {
"$ref": "#/definitions/Json"
},
"DashboardEncrypted": {
"items": {
"format": "uint8",
"type": "integer"
},
"type": "array"
},
"DeleteKey": {
"type": "string"
},
"Expires": {
"format": "date-time",
"type": "string"
},
"External": {
"type": "boolean"
},
"ExternalDeleteUrl": {
"type": "string"
},
"ExternalUrl": {
"type": "string"
},
"Id": {
"format": "int64",
"type": "integer"
},
"Key": {
"type": "string"
},
"Name": {
"type": "string"
},
"OrgId": {
"format": "int64",
"type": "integer"
},
"Updated": {
"format": "date-time",
"type": "string"
},
"UserId": {
"format": "int64",
"type": "integer"
}
},
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"DateTime": {
"description": "DateTime is a time but it serializes to ISO8601 format with millis\nIt knows how to read 3 different variations of a RFC3339 date time.\nMost APIs we encounter want either millisecond or second precision times.\nThis just tries to make it worry-free.",
"format": "date-time",
@@ -382,6 +484,12 @@
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"DsPermissionType": {
"description": "Datasource permission\nDescription:\n`0` - No Access\n`1` - Query\nEnum: 0,1",
"format": "int64",
"type": "integer",
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"Duration": {
"format": "int64",
"title": "Duration is a type used for marshalling durations.",
@@ -2908,7 +3016,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@@ -2930,7 +3037,9 @@
"labels",
"receiver"
],
"type": "object"
"type": "object",
"x-go-name": "AlertGroup",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroups": {
"items": {
@@ -3183,11 +3292,12 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence"
},
"type": "array"
"type": "array",
"x-go-name": "GettableSilences",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"labelSet": {
"additionalProperties": {
+112 -2
View File
@@ -1360,6 +1360,108 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"CreateDashboardSnapshotCommand": {
"type": "object",
"required": [
"dashboard"
],
"properties": {
"Result": {
"$ref": "#/definitions/DashboardSnapshot"
},
"dashboard": {
"$ref": "#/definitions/Json"
},
"deleteKey": {
"description": "Unique key used to delete the snapshot. It is different from the `key` so that only the creator can delete the snapshot. Required if `external` is `true`.",
"type": "string",
"x-go-name": "DeleteKey"
},
"expires": {
"description": "When the snapshot should expire in seconds in seconds. Default is never to expire.",
"type": "integer",
"format": "int64",
"default": 0,
"x-go-name": "Expires"
},
"external": {
"description": "these are passed when storing an external snapshot ref\nSave the snapshot on an external server rather than locally.",
"type": "boolean",
"default": false,
"x-go-name": "External"
},
"key": {
"description": "Define the unique key. Required if `external` is `true`.",
"type": "string",
"x-go-name": "Key"
},
"name": {
"description": "Snapshot name",
"type": "string",
"x-go-name": "Name"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"DashboardSnapshot": {
"description": "DashboardSnapshot model",
"type": "object",
"properties": {
"Created": {
"type": "string",
"format": "date-time"
},
"Dashboard": {
"$ref": "#/definitions/Json"
},
"DashboardEncrypted": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8"
}
},
"DeleteKey": {
"type": "string"
},
"Expires": {
"type": "string",
"format": "date-time"
},
"External": {
"type": "boolean"
},
"ExternalDeleteUrl": {
"type": "string"
},
"ExternalUrl": {
"type": "string"
},
"Id": {
"type": "integer",
"format": "int64"
},
"Key": {
"type": "string"
},
"Name": {
"type": "string"
},
"OrgId": {
"type": "integer",
"format": "int64"
},
"Updated": {
"type": "string",
"format": "date-time"
},
"UserId": {
"type": "integer",
"format": "int64"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"DateTime": {
"description": "DateTime is a time but it serializes to ISO8601 format with millis\nIt knows how to read 3 different variations of a RFC3339 date time.\nMost APIs we encounter want either millisecond or second precision times.\nThis just tries to make it worry-free.",
"type": "string",
@@ -1401,6 +1503,12 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"DsPermissionType": {
"description": "Datasource permission\nDescription:\n`0` - No Access\n`1` - Query\nEnum: 0,1",
"type": "integer",
"format": "int64",
"x-go-package": "github.com/grafana/grafana/pkg/models"
},
"Duration": {
"type": "integer",
"format": "int64",
@@ -3931,7 +4039,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@@ -3954,6 +4061,8 @@
"$ref": "#/definitions/receiver"
}
},
"x-go-name": "AlertGroup",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
@@ -4211,11 +4320,12 @@
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
},
"x-go-name": "GettableSilences",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableSilences"
},
"labelSet": {