Compare commits

...

12 Commits

Author SHA1 Message Date
konsalex
794b094044 fix lint issues 2025-11-26 16:11:31 +01:00
konsalex
91f9caa4ef Merge remote-tracking branch 'origin/main' into apiextensions-2 2025-11-26 15:46:51 +01:00
konsalex
02839523a0 mt first setup and cleanup access 2025-11-26 15:35:54 +01:00
konsalex
24b6ce3bad chore: update ff 2025-11-26 13:14:57 +01:00
konsalex
b550c12e40 chore: update codeowners 2025-11-26 13:09:28 +01:00
konsalex
d8e3dda0e2 chore: update wire 2025-11-26 13:07:45 +01:00
konsalex
e4e45b9271 rebasing 2025-11-26 11:50:37 +01:00
konsalex
3f23ae36c9 rebasing 2025-11-26 11:50:14 +01:00
konsalex
5272cfcc99 rebasing 2025-11-26 11:46:27 +01:00
konsalex
151b81361c rebasing 2025-11-26 11:42:53 +01:00
konsalex
90a830ad71 use FF and cleanup access code 2025-11-26 11:41:58 +01:00
konsalex
3a9559c8d0 rebasing 2025-11-26 11:41:50 +01:00
22 changed files with 467 additions and 29 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1322,6 +1322,7 @@ embed.go @grafana/grafana-as-code
/conf/provisioning/datasources/ @grafana/plugins-platform-backend
/conf/provisioning/plugins/ @grafana/plugins-platform-backend
/conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team
/conf/apiextensions.ini @grafana/grafana-app-platform-squad
# Security
/relyance.yaml @grafana/security-team

4
.gitignore vendored
View File

@@ -134,6 +134,10 @@ profile.cov
/pkg/operators/enterprise_*
/pkg/operators/**/enterprise_*
# Enterprise apiextensions server
pkg/registry/apis/apiextensions/*
!pkg/registry/apis/apiextensions/register.go
debug.test
/examples/*/dist
/packaging/**/*.rpm

53
conf/apiextensions.ini Normal file
View File

@@ -0,0 +1,53 @@
; Run locally unified storage with SQLite to test
; new API registration changes
app_mode = development
target = all
[log]
level = debug
[server]
; HTTPS is required for kubectl (but HTTP works for testing with curl)
protocol = https
http_port = 1111
[feature_toggles]
; Enable the apiextensions feature
apiExtensions = true
; Enable unified storage globally
unifiedStorage = true
; Enable search indexing for unified storage
unifiedStorageSearch = true
; Enable the grafana-apiserver explicitly
grafanaAPIServer = true
; Enable K8s aggregator for API discovery aggregation
; NOTE: This is an enterprise-only feature that requires TLS certificates
; This will surface the new registered group APIs to the `/apis` endpoint.
kubernetesAggregator = true
[grafana-apiserver]
; Use unified storage backed by SQL (uses your Grafana database)
storage_type = unified
; Certificates for the Kubernetes aggregator (generated by hack/make-aggregator-pki.sh)
proxy_client_cert_file = data/grafana-aggregator/client.crt
proxy_client_key_file = data/grafana-aggregator/client.key
; Configure dashboards to use unified storage
[unified_storage.dashboards.dashboard.grafana.app]
dualWriterMode = 5
; Configure folders to use unified storage (required for dashboards)
[unified_storage.folders.folder.grafana.app]
dualWriterMode = 5
[database]
; SQLite database for testing
type = sqlite3
path = grafana.db
high_availability = false
; Will only be used for the MT grafana
; apiextensions service
; [auth.extended_jwt]
; enabled = true
; jwks_url = "http://localhost:6481/jwks"

View File

@@ -407,6 +407,7 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA=
github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw=
github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
@@ -846,8 +847,10 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
@@ -896,6 +899,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88=
github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA=
github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4=
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA=
github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c=
@@ -923,6 +927,7 @@ github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU=
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco=
github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ=
@@ -939,6 +944,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
@@ -1324,6 +1330,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8=
@@ -1375,6 +1382,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs=
github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
@@ -1932,6 +1940,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -463,6 +463,10 @@ export interface FeatureToggles {
*/
kubernetesAggregatorCapTokenAuth?: boolean;
/**
* Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
*/
apiExtensions?: boolean;
/**
* Enable groupBy variable support in scenes dashboards
*/
groupByVariable?: boolean;

View File

@@ -160,6 +160,7 @@ var serviceIdentityTokenPermissions = []string{
"iam.grafana.app:*",
"preferences.grafana.app:*", // user, team, and org preferences
"collections.grafana.app:*", // user stars
"apiextensions.grafana.app:*",
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
"secret.grafana.app/securevalues:decrypt",

View File

@@ -131,19 +131,31 @@ func NamespaceKeyFunc(gr schema.GroupResource) func(ctx context.Context, name st
}
}
// NoNamespaceKeyFunc is the default function for constructing storage paths
// to a resource relative to the given prefix without a namespace.
func NoNamespaceKeyFunc(ctx context.Context, prefix string, gr schema.GroupResource, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
// ClusterScopedKeyFunc constructs storage paths for cluster-scoped resources (no namespace).
func ClusterScopedKeyFunc(gr schema.GroupResource) func(ctx context.Context, name string) (string, error) {
return func(ctx context.Context, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return key.String(), nil
}
}
// ClusterScopedKeyRootFunc is used by the generic registry store for cluster-scoped resources.
func ClusterScopedKeyRootFunc(gr schema.GroupResource) func(ctx context.Context) string {
return func(ctx context.Context) string {
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
}
return key.String()
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return prefix + key.String(), nil
}

View File

@@ -1,6 +1,8 @@
package generic
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/generic/registry"
@@ -12,16 +14,27 @@ func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, o
gv := resourceInfo.GroupVersion()
gv.Version = runtime.APIVersionInternal
strategy := NewStrategy(scheme, gv)
gr := resourceInfo.GroupResource()
var keyRootFunc func(ctx context.Context) string
var keyFunc func(ctx context.Context, name string) (string, error)
if resourceInfo.IsClusterScoped() {
strategy = strategy.WithClusterScope()
keyRootFunc = ClusterScopedKeyRootFunc(gr)
keyFunc = ClusterScopedKeyFunc(gr)
} else {
keyRootFunc = KeyRootFunc(gr)
keyFunc = NamespaceKeyFunc(gr)
}
store := &registry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: NamespaceKeyFunc(resourceInfo.GroupResource()),
KeyRootFunc: keyRootFunc,
KeyFunc: keyFunc,
PredicateFunc: Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
DefaultQualifiedResource: gr,
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: resourceInfo.TableConverter(),
CreateStrategy: strategy,

View File

@@ -15,7 +15,6 @@ import (
_ "github.com/blugelabs/bluge"
_ "github.com/blugelabs/bluge_segment_api"
_ "github.com/crewjam/saml"
_ "github.com/docker/go-connections/nat"
_ "github.com/go-jose/go-jose/v4"
_ "github.com/gobwas/glob"
_ "github.com/googleapis/gax-go/v2"
@@ -31,7 +30,6 @@ import (
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
_ "github.com/spyzhov/ajson"
_ "github.com/stretchr/testify/require"
_ "github.com/testcontainers/testcontainers-go"
_ "gocloud.dev/secrets/awskms"
_ "gocloud.dev/secrets/azurekeyvault"
_ "gocloud.dev/secrets/gcpkms"
@@ -56,7 +54,9 @@ import (
_ "github.com/grafana/e2e"
_ "github.com/grafana/gofpdf"
_ "github.com/grafana/gomemcache/memcache"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/testcontainers/testcontainers-go"
)

View File

@@ -0,0 +1,228 @@
package apiextensions
import (
"github.com/prometheus/client_golang/prometheus"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstorage "k8s.io/apiserver/pkg/server/storage"
"k8s.io/kube-openapi/pkg/common"
authlib "github.com/grafana/authlib/types"
genericregistry "k8s.io/apiserver/pkg/registry/generic"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
// CRDStorageProvider is an interface for creating CRD REST options getters.
// Enterprise provides the real implementation, OSS returns nil.
type CRDStorageProvider interface {
NewCRDRESTOptionsGetter(
delegate *apistore.RESTOptionsGetter,
unifiedClient resource.ResourceClient,
) genericregistry.RESTOptionsGetter
}
// OSSCRDStorageProvider is the OSS implementation that returns nil (feature disabled)
type OSSCRDStorageProvider struct{}
func ProvideOSSCRDStorageProvider() CRDStorageProvider {
return &OSSCRDStorageProvider{}
}
func (p *OSSCRDStorageProvider) NewCRDRESTOptionsGetter(
delegate *apistore.RESTOptionsGetter,
unifiedClient resource.ResourceClient,
) genericregistry.RESTOptionsGetter {
return nil
}
var _ builder.APIGroupBuilder = (*Builder)(nil)
// Builder implements builder.APIGroupBuilder for CustomResourceDefinitions.
// This implementation uses the Kubernetes apiextensions-apiserver for CRD handling,
// adapted to work with Grafana's unified storage backend.
//
// IMPORTANT: This builder only registers the CRD types with the scheme.
// The actual CRD storage and custom resource handling is done by the
// Kubernetes apiextensions-apiserver, which is created separately and
// chained as a delegate server.
type Builder struct {
features featuremgmt.FeatureToggles
accessClient authlib.AccessClient
unifiedClient resource.ResourceClient
apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions
storageProvider CRDStorageProvider
}
// RegisterAPIService registers the apiextensions API group in single-tenant mode
func RegisterAPIService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
accessClient authlib.AccessClient,
registerer prometheus.Registerer,
unified resource.ResourceClient,
storageProvider CRDStorageProvider,
) (*Builder, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) {
return nil, nil
}
b := &Builder{
features: features,
accessClient: accessClient,
unifiedClient: unified,
storageProvider: storageProvider,
}
// Register the builder to install the schema
apiregistration.RegisterAPI(b)
return b, nil
}
// GetAuthorizer returns the authorizer for CRD resources
// Breaks locally now for ST, will need to test in MT
// For ST just comment this out to test
// func (b *Builder) GetAuthorizer() authorizer.Authorizer {
// return grafanaauthorizer.NewServiceAuthorizer()
// }
// NewAPIService creates an Builder for multi-tenant mode
func NewAPIService(
accessClient authlib.AccessClient,
unified resource.ResourceClient,
registerer prometheus.Registerer,
features featuremgmt.FeatureToggles,
storageProvider CRDStorageProvider,
) (*Builder, error) {
return &Builder{
features: features,
accessClient: accessClient,
unifiedClient: unified,
storageProvider: storageProvider,
}, nil
}
// GetGroupVersion returns the API group version for apiextensions.k8s.io/v1
func (b *Builder) GetGroupVersion() schema.GroupVersion {
return apiextensionsv1.SchemeGroupVersion
}
// InstallSchema installs the CRD types into the scheme
func (b *Builder) InstallSchema(scheme *runtime.Scheme) error {
gv := b.GetGroupVersion()
// Register the apiextensions types from the K8s apiextensions-apiserver
// This uses the types and scheme from the K8s package
metav1.AddToGroupVersion(scheme, gv)
// Add the CRD types to the scheme
scheme.AddKnownTypes(gv,
&apiextensionsv1.CustomResourceDefinition{},
&apiextensionsv1.CustomResourceDefinitionList{},
)
return scheme.SetVersionPriority(gv)
}
func (b *Builder) AllowedV0Alpha1Resources() []string {
return nil
}
// UpdateAPIGroupInfo is a no-op for the apiextensions builder.
// The actual CRD storage is created by the apiextensions server, not the builder.
// This is called by the builder framework but we don't need to do anything here
// since we're using the K8s apiextensions-apiserver which creates its own storage.
func (b *Builder) UpdateAPIGroupInfo(
_ *genericapiserver.APIGroupInfo,
_ builder.APIGroupOptions,
) error {
// Don't install any storage here - the apiextensions server handles this
return nil
}
// GetOpenAPIDefinitions returns the OpenAPI definitions for CRD types
func (b *Builder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return apiextensionsopenapi.GetOpenAPIDefinitions(ref)
}
}
// CreateAPIExtensionsServer creates the Kubernetes apiextensions-apiserver
// This server handles CRD storage and custom resource (CR) handling.
// It should be used as a delegate for the main Grafana API server.
func (b *Builder) CreateAPIExtensionsServer(
serverConfig genericapiserver.RecommendedConfig,
delegationTarget genericapiserver.DelegationTarget,
restOptsGetter *apistore.RESTOptionsGetter,
) (*apiextensionsapiserver.CustomResourceDefinitions, error) {
if restOptsGetter == nil {
return nil, nil
}
// Create the CRD REST options getter that uses unified storage
crdRestOptsGetter := b.storageProvider.NewCRDRESTOptionsGetter(restOptsGetter, b.unifiedClient)
if crdRestOptsGetter == nil {
// Enterprise feature not available
return nil, nil
}
// Create a fresh copy of the config for the apiextensions server
// We need to clear PostStartHooks to avoid conflicts with hooks
// already registered by the main server (e.g., "playlist")
apiExtensionsGenericConfig := serverConfig
apiExtensionsGenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
// Set the RESTOptionsGetter on the GenericConfig
// The K8s apiextensions-apiserver uses GenericConfig.RESTOptionsGetter for CRD storage
// and ExtraConfig.CRDRESTOptionsGetter for Custom Resource storage
apiExtensionsGenericConfig.RESTOptionsGetter = crdRestOptsGetter
// Enable the CRD resources in the API resource config
apiResourceConfig := serverstorage.NewResourceConfig()
apiResourceConfig.EnableVersions(apiextensionsv1.SchemeGroupVersion)
apiExtensionsGenericConfig.MergedResourceConfig = apiResourceConfig
// Configure the apiextensions server
apiextensionsConfig := &apiextensionsapiserver.Config{
GenericConfig: &apiExtensionsGenericConfig,
ExtraConfig: apiextensionsapiserver.ExtraConfig{
// CRDRESTOptionsGetter is used for Custom Resource (CR) storage, not CRD storage
CRDRESTOptionsGetter: crdRestOptsGetter,
MasterCount: 1,
// Webhook conversion is not supported yet
ServiceResolver: nil,
AuthResolverWrapper: nil,
},
}
server, err := apiextensionsConfig.Complete().New(delegationTarget)
if err != nil {
return nil, err
}
b.apiExtensionsServer = server
return server, nil
}
// GetAPIExtensionsServer returns the apiextensions server (if created)
func (b *Builder) GetAPIExtensionsServer() *apiextensionsapiserver.CustomResourceDefinitions {
return b.apiExtensionsServer
}
// SetAPIServer is a no-op for compatibility with the builder interface
func (b *Builder) SetAPIServer(server *genericapiserver.GenericAPIServer) {
}

View File

@@ -1,6 +1,7 @@
package apiregistry
import (
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
"github.com/grafana/grafana/pkg/registry/apis/collections"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
@@ -20,6 +21,7 @@ type Service struct{}
// ProvideRegistryServiceSink is an entry point for each service that will force initialization
// and give each builder the chance to register itself with the main server
func ProvideRegistryServiceSink(
_ *apiextensions.Builder,
_ *dashboardinternal.DashboardsAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *datasource.DataSourceAPIBuilder,

View File

@@ -3,6 +3,7 @@ package apiregistry
import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
"github.com/grafana/grafana/pkg/registry/apis/collections"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
@@ -59,6 +60,7 @@ var WireSet = wire.NewSet(
provisioningExtras,
// Each must be added here *and* in the ServiceSink above
apiextensions.RegisterAPIService,
dashboardinternal.RegisterAPIService,
dashboardsnapshot.RegisterAPIService,
datasource.RegisterAPIService,

15
pkg/server/wire_gen.go generated
View File

@@ -48,6 +48,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/registry/apis"
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
"github.com/grafana/grafana/pkg/registry/apis/collections"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
@@ -861,6 +862,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService)
ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService)
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
crdStorageProvider := apiextensions.ProvideOSSCRDStorageProvider()
apiextensionsBuilder, err := apiextensions.RegisterAPIService(cfg, featureToggles, apiserverService, accessClient, registerer, resourceClient, crdStorageProvider)
if err != nil {
return nil, err
}
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl)
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService)
@@ -914,7 +920,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
apiregistryService := apiregistry.ProvideRegistryServiceSink(apiextensionsBuilder, dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
if err != nil {
return nil, err
@@ -1511,6 +1517,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService)
ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService)
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
crdStorageProvider := apiextensions.ProvideOSSCRDStorageProvider()
apiextensionsBuilder, err := apiextensions.RegisterAPIService(cfg, featureToggles, apiserverService, accessClient, registerer, resourceClient, crdStorageProvider)
if err != nil {
return nil, err
}
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl)
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService)
@@ -1564,7 +1575,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
apiregistryService := apiregistry.ProvideRegistryServiceSink(apiextensionsBuilder, dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
if err != nil {
return nil, err

View File

@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/registry"
apisregistry "github.com/grafana/grafana/pkg/registry/apis"
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/extras"
"github.com/grafana/grafana/pkg/registry/apis/secret"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
@@ -149,6 +150,7 @@ var wireExtsBasicSet = wire.NewSet(
sql.ProvideStorageBackend,
builder.ProvideDefaultBuildHandlerChainFuncFromBuilders,
aggregatorrunner.ProvideNoopAggregatorConfigurator,
apiextensions.ProvideOSSCRDStorageProvider,
apisregistry.WireSetExts,
gsmKMSProviders.ProvideOSSKMSProviders,
secret.ProvideSecureValueClient,

View File

@@ -3,6 +3,7 @@ package aggregatorrunner
import (
"context"
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -21,6 +22,10 @@ func (n NoopAggregatorConfigurator) Run(ctx context.Context, transport *options.
return nil, nil
}
func (n *NoopAggregatorConfigurator) SetCRDInformer(_ apiextensionsinformers.CustomResourceDefinitionInformer) {
// noop
}
func ProvideNoopAggregatorConfigurator() AggregatorRunner {
return &NoopAggregatorConfigurator{}
}

View File

@@ -3,6 +3,7 @@ package aggregatorrunner
import (
"context"
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -21,4 +22,8 @@ type AggregatorRunner interface {
// Run starts the complete apiserver chain, expects it executes any logic inside a goroutine and doesn't block. Returns the running server.
Run(ctx context.Context, transport *options.RoundTripperFunc, stoppedCh chan error) (*genericapiserver.GenericAPIServer, error)
// SetCRDInformer sets the CRD informer for auto-registering APIServices for CRDs.
// This should be called before Configure if CRD API is enabled.
SetCRDInformer(informer apiextensionsinformers.CustomResourceDefinitionInformer)
}

View File

@@ -20,6 +20,8 @@ import (
"k8s.io/client-go/tools/clientcmd"
"k8s.io/kube-openapi/pkg/common"
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
"github.com/grafana/authlib/types"
"github.com/grafana/dskit/services"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
@@ -335,6 +337,7 @@ func (s *service) start(ctx context.Context) error {
serverConfig.MaxRequestBodyBytes = MaxRequestBodyBytes
var optsregister apistore.StorageOptionsRegister
var restOptsGetter *apistore.RESTOptionsGetter
if o.StorageOptions.StorageType == grafanaapiserveroptions.StorageTypeEtcd {
if err := o.RecommendedOptions.Etcd.Validate(); len(err) > 0 {
@@ -344,9 +347,9 @@ func (s *service) start(ctx context.Context) error {
return err
}
} else {
getter := apistore.NewRESTOptionsGetterForClient(s.unified, s.secrets, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
optsregister = getter.RegisterOptions
serverConfig.RESTOptionsGetter = getter
restOptsGetter = apistore.NewRESTOptionsGetterForClient(s.unified, s.secrets, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
optsregister = restOptsGetter.RegisterOptions
serverConfig.RESTOptionsGetter = restOptsGetter
}
defGetters := []common.GetOpenAPIDefinitions{
@@ -383,8 +386,35 @@ func (s *service) start(ctx context.Context) error {
return fmt.Errorf("failed to register post start hooks for app installers: %w", err)
}
// Create the server
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
// Determine the delegate for the main server
var delegationTarget = genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)
var apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions
//nolint:staticcheck // not yet migrated to OpenFeature
apiExtensionsEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagApiExtensions)
if apiExtensionsEnabled && restOptsGetter != nil {
// Create the K8s apiextensions-apiserver for CRD/CR handling
// This server handles:
// - CRD storage (create, get, list, update, delete CRDs)
// - Custom resource handling (dynamically serves CRs based on registered CRDs)
s.log.Info("Creating apiextensions server")
apiExtensionsServer, err = s.createAPIExtensionsServer(builders, serverConfig, delegationTarget, restOptsGetter)
if err != nil {
return fmt.Errorf("failed to create apiextensions server: %w", err)
}
if apiExtensionsServer != nil {
s.log.Info("apiextensions server created successfully, chaining as delegate")
// Chain the apiextensions server as the delegate
// Requests go: grafana-apiserver -> apiextensions-apiserver -> notFoundHandler
delegationTarget = apiExtensionsServer.GenericAPIServer
} else {
s.log.Warn("apiextensions server was not created (returned nil)")
}
}
// Create the main Grafana API server
server, err := serverConfig.Complete().New("grafana-apiserver", delegationTarget)
if err != nil {
return err
}
@@ -440,6 +470,11 @@ func (s *service) start(ctx context.Context) error {
isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator)
if isKubernetesAggregatorEnabled {
// Pass CRD informer to aggregator if apiextensions is enabled (for auto-registering APIServices for CRDs)
if apiExtensionsServer != nil && apiExtensionsServer.Informers != nil {
s.log.Info("Starting CRD informer for apiextensions service")
s.aggregatorRunner.SetCRDInformer(apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions())
}
aggregatorServer, err := s.aggregatorRunner.Configure(s.options, serverConfig, delegate, s.scheme, builders)
if err != nil {
return err
@@ -644,3 +679,30 @@ func useNamespaceFromPath(path string, user *user.SignedInUser) {
}
}
}
// createAPIExtensionsServer creates the Kubernetes apiextensions-apiserver for CRD/CR handling.
// This server is chained as a delegate, handling CRD storage and dynamic custom resource serving.
func (s *service) createAPIExtensionsServer(
builders []builder.APIGroupBuilder,
serverConfig *genericapiserver.RecommendedConfig,
delegationTarget genericapiserver.DelegationTarget,
restOptsGetter *apistore.RESTOptionsGetter,
) (*apiextensionsapiserver.CustomResourceDefinitions, error) {
// Find the apiextensions Builder
type apiExtensionsCreator interface {
CreateAPIExtensionsServer(
serverConfig genericapiserver.RecommendedConfig,
delegationTarget genericapiserver.DelegationTarget,
restOptsGetter *apistore.RESTOptionsGetter,
) (*apiextensionsapiserver.CustomResourceDefinitions, error)
}
for _, b := range builders {
if creator, ok := b.(apiExtensionsCreator); ok {
return creator.CreateAPIExtensionsServer(*serverConfig, delegationTarget, restOptsGetter)
}
}
// No apiextensions builder found
return nil, nil
}

View File

@@ -755,6 +755,13 @@ var (
Owner: grafanaAppPlatformSquad,
RequiresRestart: true,
},
{
Name: "apiExtensions",
Description: "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
RequiresRestart: true,
},
{
Name: "groupByVariable",
Description: "Enable groupBy variable support in scenes dashboards",

View File

@@ -104,6 +104,7 @@ sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,fa
sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesAggregatorCapTokenAuth,experimental,@grafana/grafana-app-platform-squad,false,true,false
apiExtensions,experimental,@grafana/grafana-app-platform-squad,false,true,false
groupByVariable,experimental,@grafana/dashboards-squad,false,false,false
scopeFilters,experimental,@grafana/dashboards-squad,false,false,false
oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
104 sqlExpressionsColumnAutoComplete experimental @grafana/datapro false false true
105 kubernetesAggregator experimental @grafana/grafana-app-platform-squad false true false
106 kubernetesAggregatorCapTokenAuth experimental @grafana/grafana-app-platform-squad false true false
107 apiExtensions experimental @grafana/grafana-app-platform-squad false true false
108 groupByVariable experimental @grafana/dashboards-squad false false false
109 scopeFilters experimental @grafana/dashboards-squad false false false
110 oauthRequireSubClaim experimental @grafana/identity-access-team false false false

View File

@@ -311,6 +311,10 @@ const (
// Enable CAP token based authentication in grafana's embedded kube-aggregator
FlagKubernetesAggregatorCapTokenAuth = "kubernetesAggregatorCapTokenAuth"
// FlagApiExtensions
// Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
FlagApiExtensions = "apiExtensions"
// FlagGroupByVariable
// Enable groupBy variable support in scenes dashboards
FlagGroupByVariable = "groupByVariable"

View File

@@ -551,7 +551,6 @@
"description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
@@ -565,7 +564,6 @@
"description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
@@ -635,6 +633,19 @@
"expression": "true"
}
},
{
"metadata": {
"name": "apiExtensions",
"resourceVersion": "1764159104213",
"creationTimestamp": "2025-11-26T12:11:44Z"
},
"spec": {
"description": "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad",
"requiresRestart": true
}
},
{
"metadata": {
"name": "appPlatformGrpcClientAuth",

View File

@@ -184,6 +184,7 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
return true
}, claims.NoopZookie{}, nil
}
if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
span.SetAttributes(attribute.Bool("allowed", false))
span.SetStatus(codes.Error, "Namespace mismatch")