K8s: Add App SDK installer (#107773)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(©)
|
||||
}
|
||||
return ©, 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, ©, builders)
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user