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" "k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/registry/apis/query/queryschema" "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) { 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 return oas, nil
} }

View File

@@ -37,6 +37,7 @@ var (
// DataSourceAPIBuilder is used just so wire has something unique to return // DataSourceAPIBuilder is used just so wire has something unique to return
type DataSourceAPIBuilder struct { type DataSourceAPIBuilder struct {
datasourceResourceInfo utils.ResourceInfo datasourceResourceInfo utils.ResourceInfo
isAlias bool // The datasourceResourceInfo group is an alias and the API should be deprecated
pluginJSON plugins.JSONData pluginJSON plugins.JSONData
client PluginClient // will only ever be called with the same plugin id! client PluginClient // will only ever be called with the same plugin id!
@@ -58,10 +59,17 @@ func RegisterAPIService(
pluginSources sources.Registry, pluginSources sources.Registry,
) (*DataSourceAPIBuilder, error) { ) (*DataSourceAPIBuilder, error) {
//nolint:staticcheck // not yet migrated to OpenFeature //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 return nil, nil
} }
// Include CRUD when running local dev mode
configCrudUseNewApis := experimental
var err error var err error
var builder *DataSourceAPIBuilder var builder *DataSourceAPIBuilder
@@ -84,13 +92,25 @@ func RegisterAPIService(
accessControl, accessControl,
//nolint:staticcheck // not yet migrated to OpenFeature //nolint:staticcheck // not yet migrated to OpenFeature
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes), features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
false, configCrudUseNewApis, // register the CRUD endpoints
) )
if err != nil { if err != nil {
return nil, err 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) 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 return builder, nil // only used for wire
} }

View File

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