Dashboards: Use the OpenAPI generated by app-sdk in the manifest to … (#114858)
This commit is contained in:
1 parent
e9ba45ca4f
commit
8e11851bb0
6 files changed
+5552
-1175
No files matched your search
@@ -11,8 +11,7 @@ do-generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generati
|
||||
--tsgenpath=../../packages/grafana-schema/src/schema \
|
||||
--grouping=group \
|
||||
--defencoding=none \
|
||||
--genoperatorstate=false \
|
||||
--noschemasinmanifest
|
||||
--genoperatorstate=false
|
||||
|
||||
.PHONY: post-generate-cleanup
|
||||
post-generate-cleanup: ## Clean up the generated code
|
||||
|
||||
+24
@@ -6,6 +6,7 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -21,6 +22,24 @@ import (
|
||||
v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
||||
)
|
||||
|
||||
var (
|
||||
rawSchemaDashboardv0alpha1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`)
|
||||
versionSchemaDashboardv0alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaDashboardv0alpha1, &versionSchemaDashboardv0alpha1)
|
||||
rawSchemaSnapshotv0alpha1 = []byte(`{"Snapshot":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"dashboard":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"The raw dashboard (unstructured for now)","type":"object"},"expires":{"default":0,"description":"Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds)","type":"integer"},"external":{"default":false,"description":"When set to true, the snapshot exists in a remote server","type":"boolean"},"externalUrl":{"description":"The external URL where the snapshot can be seen","type":"string"},"originalUrl":{"description":"The URL that created the dashboard originally","type":"string"},"timestamp":{"description":"Snapshot creation timestamp","type":"string"},"title":{"description":"Snapshot title","type":"string"}},"type":"object"}}`)
|
||||
versionSchemaSnapshotv0alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaSnapshotv0alpha1, &versionSchemaSnapshotv0alpha1)
|
||||
rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`)
|
||||
versionSchemaDashboardv1beta1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1)
|
||||
rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":Line truncated
|
||||
versionSchemaDashboardv2alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1)
|
||||
rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additioLine truncated
|
||||
versionSchemaDashboardv2beta1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1)
|
||||
)
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "dashboard",
|
||||
Group: "dashboard.grafana.app",
|
||||
@@ -35,6 +54,7 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Dashboards",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaDashboardv0alpha1,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -42,6 +62,7 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Snapshots",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaSnapshotv0alpha1,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
@@ -60,6 +81,7 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Dashboards",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaDashboardv1beta1,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
@@ -78,6 +100,7 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Dashboards",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaDashboardv2alpha1,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
@@ -96,6 +119,7 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Dashboards",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaDashboardv2beta1,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
manifestdata "github.com/grafana/grafana/apps/dashboard/pkg/apis"
|
||||
internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
|
||||
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
@@ -843,6 +844,50 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti
|
||||
maps.Copy(defs, dashv1.GetOpenAPIDefinitions(ref))
|
||||
maps.Copy(defs, dashv2alpha1.GetOpenAPIDefinitions(ref))
|
||||
maps.Copy(defs, dashv2beta1.GetOpenAPIDefinitions(ref))
|
||||
md := manifestdata.LocalManifest().ManifestData
|
||||
// Overwrite the OpenAPI generated from kubernetes (sourced from the go types) with the OpenAPI generated by grafana-app-sdk
|
||||
// from the manifest CUE, as it correctly handles the CUE disjunctions in the dashboard spec.
|
||||
// We don't touch any types which were not specified in the manifest CUE (such as custom route types).
|
||||
for _, version := range md.Versions {
|
||||
// We don't need to correct the v0 or v1 openAPI as the spec type is just `any`
|
||||
if len(version.Name) > 1 && (version.Name[1] == '0' || version.Name[1] == '1') {
|
||||
continue
|
||||
}
|
||||
for _, kind := range version.Kinds {
|
||||
pkgPrefix := fmt.Sprintf("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/%s", version.Name)
|
||||
oapi, err := kind.Schema.AsKubeOpenAPI(schema.GroupVersionKind{
|
||||
Group: md.Group,
|
||||
Version: version.Name,
|
||||
Kind: kind.Kind,
|
||||
}, ref, pkgPrefix)
|
||||
if err != nil {
|
||||
logging.DefaultLogger.Error("unable to generate openAPI for kind %s: %w", kind.Kind, err)
|
||||
continue
|
||||
}
|
||||
maps.Copy(defs, oapi)
|
||||
}
|
||||
}
|
||||
|
||||
// Fix legacyOptions schema for v2alpha1 and v2beta1 to allow any value type
|
||||
// The generated schema incorrectly restricts values to objects, but map[string]interface{} can hold any type
|
||||
// This fix must be applied here so structured-merge-diff uses the correct schema
|
||||
// For some reason this issue occurs with both the kubernetes-generated openAPI sourced from go, _and_ the OpenAPI from the AppManifest
|
||||
// TODO: @IfSentient this should really be addressed in the app-sdk's generation, or work out what about this particular CUE value is broken
|
||||
for _, defKey := range []string{
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec",
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec",
|
||||
} {
|
||||
if def, ok := defs[defKey]; ok {
|
||||
if legacyOptions, ok := def.Schema.Properties["legacyOptions"]; ok {
|
||||
// Fix: Use additionalProperties: true to allow any value type (string, number, boolean, array, object, etc.)
|
||||
// instead of restricting to objects only. This must match map[string]interface{} semantics.
|
||||
legacyOptions.AdditionalProperties = &spec.SchemaOrBool{Allows: true}
|
||||
def.Schema.Properties["legacyOptions"] = legacyOptions
|
||||
defs[defKey] = def
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defs
|
||||
}
|
||||
}
|
||||
|
||||
+888
-1173
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -78,6 +78,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
||||
}, {
|
||||
Group: "dashboard.grafana.app",
|
||||
Version: "v2alpha1",
|
||||
}, {
|
||||
Group: "dashboard.grafana.app",
|
||||
Version: "v2beta1",
|
||||
}, {
|
||||
Group: "folder.grafana.app",
|
||||
Version: "v1beta1",
|
||||
|
||||
Reference in new issue
Block a user