mirror of
https://github.com/grafana/grafana.git
synced 2025-12-20 19:44:55 +08:00
Compare commits
12 Commits
zoltan/pos
...
apiextensi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794b094044 | ||
|
|
91f9caa4ef | ||
|
|
02839523a0 | ||
|
|
24b6ce3bad | ||
|
|
b550c12e40 | ||
|
|
d8e3dda0e2 | ||
|
|
e4e45b9271 | ||
|
|
3f23ae36c9 | ||
|
|
5272cfcc99 | ||
|
|
151b81361c | ||
|
|
90a830ad71 | ||
|
|
3a9559c8d0 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -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
4
.gitignore
vendored
@@ -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
53
conf/apiextensions.ini
Normal 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"
|
||||
@@ -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=
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 := ®istry.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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
228
pkg/registry/apis/apiextensions/register.go
Normal file
228
pkg/registry/apis/apiextensions/register.go
Normal 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) {
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
15
pkg/server/wire_gen.go
generated
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -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
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -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"
|
||||
|
||||
15
pkg/services/featuremgmt/toggles_gen.json
generated
15
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user