K8s: Add App SDK installer (#107773)

This commit is contained in:
Todd Treece
2025-07-21 13:32:15 -04:00
committed by GitHub
parent 548964f728
commit e5d2f92384
107 changed files with 2772 additions and 1891 deletions
+8 -5
View File
@@ -107,9 +107,11 @@ func SetupConfig(
buildCommit string,
buildBranch string,
buildHandlerChainFuncFromBuilders BuildHandlerChainFuncFromBuilders,
gvs []schema.GroupVersion,
additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions,
) error {
serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders)
defsGetter := GetOpenAPIDefinitions(builders)
defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...)
serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(defsGetter),
openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme))
@@ -119,7 +121,7 @@ func SetupConfig(
openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme))
// Add the custom routes to service discovery
serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders)
serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs)
serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) {
meta := r.Metadata()
kind := ""
@@ -226,6 +228,7 @@ func SetupConfig(
// Set the swagger build versions
serverConfig.OpenAPIConfig.Info.Title = "Grafana API Server"
serverConfig.OpenAPIConfig.Info.Version = buildVersion
serverConfig.OpenAPIV3Config.Info.Title = "Grafana API Server"
serverConfig.OpenAPIV3Config.Info.Version = buildVersion
serverConfig.SkipOpenAPIInstallation = false
@@ -278,13 +281,13 @@ func InstallAPIs(
dualWriteService dualwrite.Service,
optsregister apistore.StorageOptionsRegister,
features featuremgmt.FeatureToggles,
dualWriterMetrics *grafanarest.DualWriterMetrics,
builderMetrics *BuilderMetrics,
) error {
// dual writing is only enabled when the storage type is not legacy.
// this is needed to support setting a default RESTOptionsGetter for new APIs that don't
// support the legacy storage type.
var dualWrite grafanarest.DualWriteBuilder
metrics := newBuilderMetrics(reg)
dualWriterMetrics := grafanarest.NewDualWriterMetrics(reg)
// nolint:staticcheck
if storageOpts.StorageType != options.StorageTypeLegacy {
@@ -346,7 +349,7 @@ func InstallAPIs(
return nil, err
}
metrics.recordDualWriterModes(gr.Resource, gr.Group, mode, currentMode)
builderMetrics.RecordDualWriterModes(gr.Resource, gr.Group, mode, currentMode)
switch currentMode {
case grafanarest.Mode0:
+8 -4
View File
@@ -6,13 +6,13 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)
type builderMetrics struct {
type BuilderMetrics struct {
dualWriterTargetMode *prometheus.GaugeVec
dualWriterCurrentMode *prometheus.GaugeVec
}
func newBuilderMetrics(reg prometheus.Registerer) *builderMetrics {
return &builderMetrics{
func ProvideBuilderMetrics(reg prometheus.Registerer) *BuilderMetrics {
return &BuilderMetrics{
dualWriterTargetMode: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
Name: "unified_storage_dual_writer_target_mode",
Help: "Unified Storage dual writer target mode",
@@ -24,7 +24,11 @@ func newBuilderMetrics(reg prometheus.Registerer) *builderMetrics {
}
}
func (m *builderMetrics) recordDualWriterModes(resource, group string, targetMode, currentMode grafanarest.DualWriterMode) {
func (m *BuilderMetrics) RecordDualWriterModes(resource, group string, targetMode, currentMode grafanarest.DualWriterMode) {
m.dualWriterTargetMode.WithLabelValues(resource, group).Set(float64(targetMode))
m.dualWriterCurrentMode.WithLabelValues(resource, group).Set(float64(currentMode))
}
func ProvideDualWriterMetrics(reg prometheus.Registerer) *grafanarest.DualWriterMetrics {
return grafanarest.NewDualWriterMetrics(reg)
}
+157 -141
View File
@@ -4,6 +4,7 @@ import (
"maps"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
openapi "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
spec "k8s.io/kube-openapi/pkg/validation/spec"
@@ -14,11 +15,18 @@ import (
)
// This should eventually live in grafana-app-sdk
func GetOpenAPIDefinitions(builders []APIGroupBuilder) openapi.GetOpenAPIDefinitions {
func GetOpenAPIDefinitions(builders []APIGroupBuilder, additionalGetters ...openapi.GetOpenAPIDefinitions) openapi.GetOpenAPIDefinitions {
return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition {
defs := common.GetOpenAPIDefinitions(ref) // common grafana apis
maps.Copy(defs, data.GetOpenAPIDefinitions(ref))
maps.Copy(defs, secret.GetOpenAPIDefinitions(ref)) // Expose secret reference to all resources
for _, getter := range additionalGetters {
if getter != nil {
maps.Copy(defs, getter(ref))
}
}
// TODO: add timerange to upstream SDK setup
maps.Copy(defs, map[string]openapi.OpenAPIDefinition{
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.TimeRange": {
@@ -41,157 +49,165 @@ func GetOpenAPIDefinitions(builders []APIGroupBuilder) openapi.GetOpenAPIDefinit
}
}
func addBuilderRoutes(
targetGroupVersion schema.GroupVersion,
openAPISpec *spec3.OpenAPI,
apiGroupBuilders []APIGroupBuilder,
) (*spec3.OpenAPI, error) {
for _, apiGroupBuilder := range apiGroupBuilders {
// Optionally include raw http handlers for all builders
for _, gv := range GetGroupVersions(apiGroupBuilder) {
if gv != targetGroupVersion {
continue // Only add routes for the target group version
}
provider, ok := apiGroupBuilder.(APIGroupRouteProvider)
if ok && provider != nil {
routes := provider.GetAPIRoutes(gv)
if routes != nil {
for _, route := range routes.Root {
openAPISpec.Paths.Paths["/apis/"+gv.String()+"/"+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
for _, route := range routes.Namespace {
openAPISpec.Paths.Paths["/apis/"+gv.String()+"/namespaces/{namespace}/"+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
}
}
// Support direct manipulation of API results
processor, ok := apiGroupBuilder.(OpenAPIPostProcessor)
if ok {
return processor.PostProcessOpenAPI(openAPISpec)
}
}
}
return openAPISpec, nil
}
// Modify the OpenAPI spec to include the additional routes.
// Currently this requires: https://github.com/kubernetes/kube-openapi/pull/420
// In future k8s release, the hook will use Config3 rather than the same hook for both v2 and v3
// nolint:gocyclo
func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) {
if s.Paths == nil {
return s, nil
}
for _, b := range builders {
for _, gv := range GetGroupVersions(b) {
prefix := "/apis/" + gv.String() + "/"
if s.Paths.Paths[prefix] != nil {
copy := spec3.OpenAPI{
Version: s.Version,
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: gv.String(),
Version: version,
},
for _, gv := range gvs {
prefix := "/apis/" + gv.String() + "/"
if s.Paths.Paths[prefix] != nil {
copy := spec3.OpenAPI{
Version: s.Version,
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: gv.String(),
Version: version,
},
Components: s.Components,
ExternalDocs: s.ExternalDocs,
Servers: s.Servers,
Paths: s.Paths,
}
for k, v := range copy.Paths.Paths {
if k == prefix {
continue // API discovery
}
// Remove the deprecated watch URL -- can use list with ?watch=true
if strings.HasPrefix(k, prefix+"watch/") {
delete(copy.Paths.Paths, k)
continue
}
// Remove the "for all namespaces" global routes from OpenAPI (v3)
if !strings.HasPrefix(k, prefix+"namespaces/") {
delete(copy.Paths.Paths, k)
continue
}
// Delete has all parameters in the query string already
if v.Delete != nil {
action, ok := v.Delete.Extensions.GetString("x-kubernetes-action")
if ok && (action == "deletecollection" || action == "delete") {
v.Delete.RequestBody = nil // duplicates all the parameters
}
}
// Replace any */* media types with json+yaml (protobuf?)
ops := []*spec3.Operation{v.Delete, v.Put, v.Post}
for _, op := range ops {
if op == nil || op.RequestBody == nil || len(op.RequestBody.Content) != 1 {
continue
}
content, ok := op.RequestBody.Content["*/*"]
if ok {
op.RequestBody.Content = map[string]*spec3.MediaType{
"application/json": content,
"application/yaml": content,
"application/vnd.kubernetes.protobuf": content,
}
}
}
}
sub := copy.Paths.Paths[prefix]
if sub != nil && sub.Get != nil {
sub.Get.Tags = []string{"API Discovery"}
sub.Get.Description = "Describe the available kubernetes resources"
}
// Remove the growing list of kinds
for k, v := range copy.Components.Schemas {
if v.Extensions == nil {
continue
}
if strings.HasPrefix(k, "io.k8s.apimachinery.pkg.apis.meta.v1") {
delete(v.Extensions, "x-kubernetes-group-version-kind") // a growing list of everything
continue
}
// Remove the internal annotations
val, ok := v.Extensions["x-kubernetes-group-version-kind"]
if ok {
gvks, ok := val.([]any)
if ok {
keep := make([]map[string]any, 0, len(gvks))
for _, val := range gvks {
gvk, ok := val.(map[string]any)
if ok && gvk["version"] == "__internal" {
continue
}
keep = append(keep, gvk)
}
v.Extensions["x-kubernetes-group-version-kind"] = keep
}
}
}
// Optionally include raw http handlers
provider, ok := b.(APIGroupRouteProvider)
if ok && provider != nil {
routes := provider.GetAPIRoutes(gv)
if routes != nil {
for _, route := range routes.Root {
copy.Paths.Paths[prefix+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
for _, route := range routes.Namespace {
copy.Paths.Paths[prefix+"namespaces/{namespace}/"+route.Path] = &spec3.Path{
PathProps: *route.Spec,
}
}
}
}
// Make the sub-resources (connect) share the same tags as the main resource
for path, spec := range copy.Paths.Paths {
idx := strings.LastIndex(path, "{name}/")
if idx > 0 {
parent := copy.Paths.Paths[path[:idx+6]]
if parent != nil && parent.Get != nil {
for _, op := range GetPathOperations(spec) {
if op != nil && op.Extensions != nil {
action, ok := op.Extensions.GetString("x-kubernetes-action")
if ok && action == "connect" {
op.Tags = parent.Get.Tags
}
}
}
}
}
}
// Support direct manipulation of API results
processor, ok := b.(OpenAPIPostProcessor)
if ok {
return processor.PostProcessOpenAPI(&copy)
}
return &copy, nil
},
Components: s.Components,
ExternalDocs: s.ExternalDocs,
Servers: s.Servers,
Paths: s.Paths,
}
for k, v := range copy.Paths.Paths {
if k == prefix {
continue // API discovery
}
// Remove the deprecated watch URL -- can use list with ?watch=true
if strings.HasPrefix(k, prefix+"watch/") {
delete(copy.Paths.Paths, k)
continue
}
// Remove the "for all namespaces" global routes from OpenAPI (v3)
if !strings.HasPrefix(k, prefix+"namespaces/") {
delete(copy.Paths.Paths, k)
continue
}
// Delete has all parameters in the query string already
if v.Delete != nil {
action, ok := v.Delete.Extensions.GetString("x-kubernetes-action")
if ok && (action == "deletecollection" || action == "delete") {
v.Delete.RequestBody = nil // duplicates all the parameters
}
}
// Replace any */* media types with json+yaml (protobuf?)
ops := []*spec3.Operation{v.Delete, v.Put, v.Post}
for _, op := range ops {
if op == nil || op.RequestBody == nil || len(op.RequestBody.Content) != 1 {
continue
}
content, ok := op.RequestBody.Content["*/*"]
if ok {
op.RequestBody.Content = map[string]*spec3.MediaType{
"application/json": content,
"application/yaml": content,
"application/vnd.kubernetes.protobuf": content,
}
}
}
}
sub := copy.Paths.Paths[prefix]
if sub != nil && sub.Get != nil {
sub.Get.Tags = []string{"API Discovery"}
sub.Get.Description = "Describe the available kubernetes resources"
}
// Remove the growing list of kinds
for k, v := range copy.Components.Schemas {
if v.Extensions == nil {
continue
}
if strings.HasPrefix(k, "io.k8s.apimachinery.pkg.apis.meta.v1") {
delete(v.Extensions, "x-kubernetes-group-version-kind") // a growing list of everything
continue
}
// Remove the internal annotations
val, ok := v.Extensions["x-kubernetes-group-version-kind"]
if ok {
gvks, ok := val.([]any)
if ok {
keep := make([]map[string]any, 0, len(gvks))
for _, val := range gvks {
gvk, ok := val.(map[string]any)
if ok && gvk["version"] == "__internal" {
continue
}
keep = append(keep, gvk)
}
v.Extensions["x-kubernetes-group-version-kind"] = keep
}
}
}
// Make the sub-resources (connect) share the same tags as the main resource
for path, spec := range copy.Paths.Paths {
idx := strings.LastIndex(path, "{name}/")
if idx > 0 {
parent := copy.Paths.Paths[path[:idx+6]]
if parent != nil && parent.Get != nil {
for _, op := range GetPathOperations(spec) {
if op != nil && op.Extensions != nil {
action, ok := op.Extensions.GetString("x-kubernetes-action")
if ok && action == "connect" {
op.Tags = parent.Get.Tags
}
}
}
}
}
}
return addBuilderRoutes(gv, &copy, builders)
}
}
return s, nil
}
}