mirror of
https://github.com/grafana/grafana.git
synced 2026-06-10 09:01:30 -04:00
304 lines
7.7 KiB
Go
304 lines
7.7 KiB
Go
package openapi
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
"k8s.io/kube-openapi/pkg/spec3"
|
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/experimental/pluginschema"
|
|
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
|
)
|
|
|
|
const app_INSTANCE_NAME = "instance"
|
|
|
|
type PluginOptions struct {
|
|
Schema *pluginschema.PluginSchema
|
|
|
|
// The full resource config (spec and secure are children)
|
|
Resource *spec.Schema
|
|
|
|
// The name where the resource spec should be registered
|
|
// eg: DataSourceSpec | AppPluginSpec
|
|
SpecName string
|
|
|
|
// root+"namespaces/{namespace}/datasources"
|
|
// This is used for the POST examples
|
|
Path string
|
|
|
|
// When the value is an app, we expect {namespace}/app/instance
|
|
IsApp bool
|
|
}
|
|
|
|
// nolint:gocyclo
|
|
func AugmentOpenAPI(oas *spec3.OpenAPI, opts PluginOptions) (*spec3.OpenAPI, error) {
|
|
if opts.Schema.IsZero() {
|
|
return oas, nil // nothing special
|
|
}
|
|
|
|
// Find the root path
|
|
cfg := oas.Paths.Paths[opts.Path]
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("no route registered: %s", opts.Path)
|
|
}
|
|
if cfg.Post == nil {
|
|
return nil, fmt.Errorf("expecting POST under: %s", opts.Path)
|
|
}
|
|
|
|
// Replace the generic DataSourceSpec with the explicit one
|
|
settings := opts.Schema.SettingsSchema
|
|
if !settings.IsZero() {
|
|
resourceSpec := settings.Spec
|
|
if opts.IsApp {
|
|
resourceSpec = &spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Type: []string{"object"},
|
|
Properties: map[string]spec.Schema{
|
|
"pinned": *spec.BooleanProperty().WithDescription("shows up in the sidebar"),
|
|
"enabled": *spec.BooleanProperty().WithDescription("can be executed"),
|
|
"jsonData": *settings.Spec,
|
|
},
|
|
},
|
|
}
|
|
|
|
example := map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": app_INSTANCE_NAME,
|
|
},
|
|
"spec": map[string]any{
|
|
"enabled": true,
|
|
"pinned": true,
|
|
// JSONData (from examples)
|
|
},
|
|
}
|
|
opts.Resource.Example = example
|
|
}
|
|
|
|
oas.Components.Schemas[opts.SpecName] = resourceSpec
|
|
opts.Resource.Properties["spec"] = spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Ref: spec.MustCreateRef("#/components/schemas/" + opts.SpecName),
|
|
},
|
|
}
|
|
|
|
if len(settings.SecureValues) > 0 {
|
|
example := common.InlineSecureValues{}
|
|
ref := spec.MustCreateRef("#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue")
|
|
secure := &spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Properties: make(map[string]spec.Schema),
|
|
AdditionalProperties: &spec.SchemaOrBool{Allows: false},
|
|
}}
|
|
|
|
for _, v := range settings.SecureValues {
|
|
secure.Properties[v.Key] = spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Description: v.Description,
|
|
Ref: ref,
|
|
},
|
|
}
|
|
if v.Required {
|
|
secure.Required = append(secure.Required, v.Key)
|
|
example[v.Key] = common.InlineSecureValue{Create: "***"}
|
|
}
|
|
}
|
|
|
|
if len(example) > 0 {
|
|
secure.Example = example
|
|
}
|
|
|
|
// Link the explicit secure values in the resource
|
|
oas.Components.Schemas["SecureValues"] = secure
|
|
opts.Resource.Properties["secure"] = spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Ref: spec.MustCreateRef("#/components/schemas/SecureValues"),
|
|
},
|
|
}
|
|
}
|
|
|
|
examples := opts.Schema.SettingsExamples
|
|
if !examples.IsZero() {
|
|
for _, c := range cfg.Post.RequestBody.Content {
|
|
c.Examples = examples.Examples
|
|
}
|
|
}
|
|
}
|
|
|
|
routes := opts.Schema.Routes
|
|
if routes.IsZero() {
|
|
routes = &pluginschema.Routes{}
|
|
}
|
|
|
|
// Add custom schemas
|
|
if routes.Components != nil {
|
|
copyComponents(routes.Components, oas.Components)
|
|
}
|
|
|
|
if err := routes.AssertPrefixes("/resources", "/proxy"); err != nil {
|
|
return oas, err
|
|
}
|
|
|
|
routePrefix := opts.Path + "/{name}"
|
|
cfg = oas.Paths.Paths[routePrefix]
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("expecting route registered: %s", routePrefix)
|
|
}
|
|
if cfg.Get == nil {
|
|
return nil, fmt.Errorf("expecting GET under: %s/{name}", routePrefix)
|
|
}
|
|
|
|
var params []*spec3.Parameter
|
|
for _, p := range cfg.Parameters {
|
|
if p.Name == "namespace" {
|
|
params = append(params, p)
|
|
}
|
|
if p.Name == "name" && !opts.IsApp {
|
|
params = append(params, p)
|
|
}
|
|
}
|
|
|
|
if opts.IsApp {
|
|
// Hide the non-instance based routes
|
|
delete(oas.Paths.Paths, opts.Path)
|
|
|
|
removeName := func(pp []*spec3.Parameter) (ret []*spec3.Parameter) {
|
|
for _, p := range pp {
|
|
if p.Name != "name" {
|
|
ret = append(ret, p)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Replace the {name} property with /instance path
|
|
appRoutePrefix := opts.Path + "/" + app_INSTANCE_NAME
|
|
for k, v := range oas.Paths.Paths {
|
|
if strings.HasPrefix(k, routePrefix) {
|
|
delete(oas.Paths.Paths, k)
|
|
k = strings.Replace(k, routePrefix, appRoutePrefix, 1)
|
|
v.Parameters = removeName(v.Parameters)
|
|
oas.Paths.Paths[k] = v
|
|
}
|
|
}
|
|
routePrefix = appRoutePrefix
|
|
}
|
|
|
|
// When a schema is configured, remove the default mappings
|
|
if len(routes.Paths) > 0 {
|
|
delete(oas.Paths.Paths, routePrefix+"/resources")
|
|
delete(oas.Paths.Paths, routePrefix+"/proxy")
|
|
}
|
|
|
|
// Add all the paths
|
|
caser := cases.Title(language.English)
|
|
for k, v := range routes.Paths {
|
|
tag := caser.String(k[1:]) // "Resources", "Proxy"
|
|
if idx := strings.Index(tag, "/"); idx > 0 {
|
|
tag = tag[:idx]
|
|
}
|
|
v.Parameters = append(params, v.Parameters...)
|
|
for m, op := range getPathOperations(&v.PathProps) {
|
|
if op.Extensions == nil {
|
|
op.Extensions = make(spec.Extensions)
|
|
}
|
|
if !slices.Contains(op.Tags, tag) {
|
|
op.Tags = append(op.Tags, tag)
|
|
}
|
|
tmp := strings.ReplaceAll(strings.ReplaceAll(k, "{", ""), "}", "")
|
|
op.OperationId = fmt.Sprintf("%s%s", strings.ToLower(m), strings.ReplaceAll(tmp, "/", "_"))
|
|
}
|
|
|
|
oas.Paths.Paths[routePrefix+k] = v
|
|
}
|
|
return oas, nil
|
|
}
|
|
|
|
// getPathOperations returns the set of non-nil operations defined on a path.
|
|
// Equivalent to builder.GetPathOperations in the root module, inlined here to
|
|
// avoid a cross-module dependency that breaks go mod tidy.
|
|
func getPathOperations(path *spec3.PathProps) map[string]*spec3.Operation {
|
|
ops := make(map[string]*spec3.Operation)
|
|
if path.Get != nil {
|
|
ops[http.MethodGet] = path.Get
|
|
}
|
|
if path.Head != nil {
|
|
ops[http.MethodHead] = path.Head
|
|
}
|
|
if path.Delete != nil {
|
|
ops[http.MethodDelete] = path.Delete
|
|
}
|
|
if path.Post != nil {
|
|
ops[http.MethodPost] = path.Post
|
|
}
|
|
if path.Put != nil {
|
|
ops[http.MethodPut] = path.Put
|
|
}
|
|
if path.Patch != nil {
|
|
ops[http.MethodPatch] = path.Patch
|
|
}
|
|
if path.Trace != nil {
|
|
ops[http.MethodTrace] = path.Trace
|
|
}
|
|
if path.Options != nil {
|
|
ops[http.MethodOptions] = path.Options
|
|
}
|
|
return ops
|
|
}
|
|
|
|
// safely copy components from src to dst
|
|
func copyComponents(src *spec3.Components, dst *spec3.Components) {
|
|
if src.Schemas != nil {
|
|
if dst.Schemas == nil {
|
|
dst.Schemas = make(map[string]*spec.Schema)
|
|
}
|
|
maps.Copy(dst.Schemas, src.Schemas)
|
|
}
|
|
|
|
if src.Responses != nil {
|
|
if dst.Responses == nil {
|
|
dst.Responses = make(map[string]*spec3.Response)
|
|
}
|
|
maps.Copy(dst.Responses, src.Responses)
|
|
}
|
|
|
|
if src.Examples != nil {
|
|
if dst.Examples == nil {
|
|
dst.Examples = make(map[string]*spec3.Example)
|
|
}
|
|
maps.Copy(dst.Examples, src.Examples)
|
|
}
|
|
|
|
if src.Headers != nil {
|
|
if dst.Headers == nil {
|
|
dst.Headers = make(map[string]*spec3.Header)
|
|
}
|
|
maps.Copy(dst.Headers, src.Headers)
|
|
}
|
|
|
|
if src.Parameters != nil {
|
|
if dst.Parameters == nil {
|
|
dst.Parameters = make(map[string]*spec3.Parameter)
|
|
}
|
|
maps.Copy(dst.Parameters, src.Parameters)
|
|
}
|
|
|
|
if src.Links != nil {
|
|
if dst.Links == nil {
|
|
dst.Links = make(map[string]*spec3.Link)
|
|
}
|
|
maps.Copy(dst.Links, src.Links)
|
|
}
|
|
|
|
if src.RequestBodies != nil {
|
|
if dst.RequestBodies == nil {
|
|
dst.RequestBodies = make(map[string]*spec3.RequestBody)
|
|
}
|
|
maps.Copy(dst.RequestBodies, src.RequestBodies)
|
|
}
|
|
}
|