Compare commits

...

1 Commits

Author SHA1 Message Date
Ryan McKinley
72eec4e995 register multiple apis datasource 2025-11-18 21:02:06 +03:00
4 changed files with 140 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/registry/apis/query/queryschema"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
)
func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
@@ -70,5 +71,15 @@ func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op
},
}
// Mark all alias APIServers as deprecated
if b.isAlias {
oas.Info.Description = "Deprecated. Please use: " + b.pluginJSON.ID
for _, p := range oas.Paths.Paths {
for _, op := range builder.GetPathOperations(p) {
op.Deprecated = true
}
}
}
return oas, nil
}

View File

@@ -37,6 +37,7 @@ var (
// DataSourceAPIBuilder is used just so wire has something unique to return
type DataSourceAPIBuilder struct {
datasourceResourceInfo utils.ResourceInfo
isAlias bool // The datasourceResourceInfo group is an alias and the API should be deprecated
pluginJSON plugins.JSONData
client PluginClient // will only ever be called with the same plugin id!
@@ -58,10 +59,17 @@ func RegisterAPIService(
pluginSources sources.Registry,
) (*DataSourceAPIBuilder, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections) && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
useQueryService := !features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections)
//nolint:staticcheck // not yet migrated to OpenFeature
experimental := features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)
if !experimental && !useQueryService {
return nil, nil
}
// Include CRUD when running local dev mode
configCrudUseNewApis := experimental
var err error
var builder *DataSourceAPIBuilder
@@ -84,13 +92,25 @@ func RegisterAPIService(
accessControl,
//nolint:staticcheck // not yet migrated to OpenFeature
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
false,
configCrudUseNewApis, // register the CRUD endpoints
)
if err != nil {
return nil, err
}
// The group is a calculated FQDN (eg testdata.datasource.grafana.app)
// and should be replaced with with the raw plugin ID
aliasIDs := append(pluginJSON.AliasIDs, builder.datasourceResourceInfo.GroupResource().Group)
builder.datasourceResourceInfo = builder.datasourceResourceInfo.WithGroupAndShortName(pluginJSON.ID, "")
apiRegistrar.RegisterAPI(builder)
// Register a deprecated copy with the previous routes
for _, aliasId := range aliasIDs {
copy := *builder
copy.isAlias = true
copy.datasourceResourceInfo = builder.datasourceResourceInfo.WithGroupAndShortName(aliasId, "")
apiRegistrar.RegisterAPI(&copy)
}
}
return builder, nil // only used for wire
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"maps"
"net/http"
"strings"
"sync"
@@ -217,11 +218,9 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s
parent := copy.Paths.Paths[path[:idx+6]]
if parent != nil && parent.Get != nil {
for _, op := range GetPathOperations(spec) {
if op != nil && op.Extensions != nil {
action, ok := op.Extensions.GetString("x-kubernetes-action")
if ok && action == "connect" {
op.Tags = parent.Get.Tags
}
action, ok := op.Extensions.GetString("x-kubernetes-action")
if ok && action == "connect" {
op.Tags = parent.Get.Tags
}
}
}
@@ -234,15 +233,32 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s
}
}
func GetPathOperations(path *spec3.Path) []*spec3.Operation {
return []*spec3.Operation{
path.Get,
path.Head,
path.Delete,
path.Patch,
path.Post,
path.Put,
path.Trace,
path.Options,
// GetPathOperations returns the set of non-nil operations defined on a path
func GetPathOperations(path *spec3.Path) 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
}

View File

@@ -0,0 +1,76 @@
package builder
import (
"slices"
"strings"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/kube-openapi/pkg/spec3"
)
func TestOpenAPI_GetPathOperations(t *testing.T) {
testCases := []struct {
name string
input *spec3.Path
expect []string // the methods we should see
exclude []string // the methods we should never see
}{
{
name: "some operations",
input: &spec3.Path{
PathProps: spec3.PathProps{
Get: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "get"}},
Post: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "post"}},
Delete: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "delete"}},
},
},
expect: []string{"GET", "POST", "DELETE"},
exclude: []string{"PUT", "PATCH", "OPTIONS", "HEAD", "TRACE"},
},
{
name: "all operations",
input: &spec3.Path{
PathProps: spec3.PathProps{
Get: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "get"}},
Post: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "post"}},
Delete: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "delete"}},
Put: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "put"}},
Patch: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "patch"}},
Options: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "options"}},
Head: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "head"}},
Trace: &spec3.Operation{OperationProps: spec3.OperationProps{Summary: "trace"}},
},
},
expect: []string{"GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD", "TRACE"},
exclude: []string{},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
expect := make(map[string]bool)
for _, k := range tt.expect {
expect[k] = true
}
for k, op := range GetPathOperations(tt.input) {
require.NotNil(t, op)
require.Equal(t, strings.ToLower(k), op.Summary)
if !expect[k] {
if slices.Contains(tt.expect, k) {
require.Fail(t, "method returned multiple times", k)
} else {
require.Fail(t, "unexpected method", k)
}
}
delete(expect, k)
require.NotContains(t, tt.exclude, k, "exclude")
}
if len(expect) > 0 {
require.Fail(t, "missing expected method", expect)
}
})
}
}