mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Add example api service (#75911)
This commit is contained in:
parent
0a50ca7231
commit
717a9dd616
@ -128,6 +128,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source |
|
||||
| `metricsSummary` | Enables metrics summary queries in the Tempo data source |
|
||||
| `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources |
|
||||
| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server |
|
||||
| `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end |
|
||||
| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder |
|
||||
| `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI |
|
||||
|
10
go.mod
10
go.mod
@ -25,6 +25,10 @@ replace go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.16
|
||||
// import that instead of v0.X even though v0.X is newer.
|
||||
replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.43.0
|
||||
|
||||
// Includes https://github.com/kubernetes/kube-openapi/pull/420
|
||||
// This will not be required in the next k8s release
|
||||
replace k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f => github.com/ryantxu/kube-openapi v0.0.0-20230824154605-fe0f3703fd8d // @grafana/grafana-app-platform-squad
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.30.1 // @grafana/backend-platform
|
||||
cuelang.org/go v0.6.0-0.dev // @grafana/grafana-as-code
|
||||
@ -121,7 +125,7 @@ require (
|
||||
gopkg.in/mail.v2 v2.3.1 // @grafana/backend-platform
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend
|
||||
xorm.io/builder v0.3.6 // indirect; @grafana/backend-platform
|
||||
xorm.io/builder v0.3.6 // @grafana/backend-platform
|
||||
xorm.io/core v0.7.3 // @grafana/backend-platform
|
||||
xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend
|
||||
)
|
||||
@ -168,12 +172,12 @@ require (
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/flatbuffers v23.1.21+incompatible // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.11.0 // @grafana/backend-platform
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // @grafana/backend-platform
|
||||
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect; @grafana/grafana-as-code
|
||||
github.com/hashicorp/go-multierror v1.1.1 // @grafana/grafana-as-code
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -2650,6 +2650,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/ryantxu/kube-openapi v0.0.0-20230824154605-fe0f3703fd8d h1:/aSzsDXklM83yExguLXwl/nMrW8/7Lgefg2q632YynY=
|
||||
github.com/ryantxu/kube-openapi v0.0.0-20230824154605-fe0f3703fd8d/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
|
||||
@ -4084,8 +4086,6 @@ k8s.io/kms v0.27.1 h1:JTSQbJb+mcobScQwF0bOmZhIwP17k8GvBsiLlA6SQqw=
|
||||
k8s.io/kms v0.27.1/go.mod h1:VuTsw0uHlSycKLCkypCGxfFCjLfzf/5YMeATECd/zJA=
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
|
||||
k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
|
||||
k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
||||
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
|
@ -109,6 +109,7 @@ export interface FeatureToggles {
|
||||
traceQLStreaming?: boolean;
|
||||
metricsSummary?: boolean;
|
||||
grafanaAPIServer?: boolean;
|
||||
grafanaAPIServerWithExperimentalAPIs?: boolean;
|
||||
featureToggleAdminPage?: boolean;
|
||||
awsAsyncQueryCaching?: boolean;
|
||||
splitScopes?: boolean;
|
||||
|
10
pkg/apis/example/v0alpha1/doc.go
Normal file
10
pkg/apis/example/v0alpha1/doc.go
Normal file
@ -0,0 +1,10 @@
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +groupName=example.grafana.com
|
||||
|
||||
// The testing api is a dependency free service that we can use to experiment with
|
||||
// api aggregation across multiple deployment models. Specifically:
|
||||
// - standalone: running as part of the standard grafana build
|
||||
// - aggregated: running as the target
|
||||
|
||||
package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
|
89
pkg/apis/example/v0alpha1/openapi.go
Normal file
89
pkg/apis/example/v0alpha1/openapi.go
Normal file
@ -0,0 +1,89 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
// NOTE: this must match the golang fully qualified name!
|
||||
const kindKey = "github.com/grafana/grafana/pkg/apis/example/v0alpha1.RuntimeInfo"
|
||||
|
||||
func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
kindKey: schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref),
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"startupTime": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"buildVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"buildCommit": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"enterpriseBuildCommit": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"buildBranch": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"buildStamp": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"enterprise": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"packaging": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
195
pkg/apis/example/v0alpha1/register.go
Normal file
195
pkg/apis/example/v0alpha1/register.go
Normal file
@ -0,0 +1,195 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
// GroupName is the group name for this API.
|
||||
const GroupName = "example.grafana.com"
|
||||
const VersionID = "v0alpha1" //
|
||||
const APIVersion = GroupName + "/" + VersionID
|
||||
|
||||
var _ grafanaapiserver.APIGroupBuilder = (*TestingAPIBuilder)(nil)
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type TestingAPIBuilder struct {
|
||||
codecs serializer.CodecFactory
|
||||
}
|
||||
|
||||
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration grafanaapiserver.APIRegistrar) *TestingAPIBuilder {
|
||||
if !features.IsEnabled(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
builder := &TestingAPIBuilder{}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
func (b *TestingAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return SchemeGroupVersion
|
||||
}
|
||||
|
||||
func (b *TestingAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
err := AddToScheme(scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return scheme.SetVersionPriority(SchemeGroupVersion)
|
||||
}
|
||||
|
||||
func (b *TestingAPIBuilder) GetAPIGroupInfo(
|
||||
scheme *runtime.Scheme,
|
||||
codecs serializer.CodecFactory, // pointer?
|
||||
optsGetter generic.RESTOptionsGetter,
|
||||
) (*genericapiserver.APIGroupInfo, error) {
|
||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs)
|
||||
storage := map[string]rest.Storage{}
|
||||
storage["runtime"] = newDeploymentInfoStorage()
|
||||
apiGroupInfo.VersionedResourcesStorageMap[VersionID] = storage
|
||||
b.codecs = codecs
|
||||
return &apiGroupInfo, nil
|
||||
}
|
||||
|
||||
func (b *TestingAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
return getOpenAPIDefinitions
|
||||
}
|
||||
|
||||
// Register additional routes with the server
|
||||
func (b *TestingAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
|
||||
return &grafanaapiserver.APIRoutes{
|
||||
Root: []grafanaapiserver.APIRouteHandler{
|
||||
{
|
||||
Path: "/aaa",
|
||||
Spec: &spec3.PathProps{
|
||||
Summary: "an example at the root level",
|
||||
Description: "longer description here?",
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
Parameters: []*spec3.Parameter{
|
||||
{ParameterProps: spec3.ParameterProps{
|
||||
Name: "a",
|
||||
}},
|
||||
},
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
StatusCodeResponses: map[int]*spec3.Response{
|
||||
200: {
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "OK",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"text/plain": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("Root level handler (aaa)"))
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/bbb",
|
||||
Spec: &spec3.PathProps{
|
||||
Summary: "an example at the root level",
|
||||
Description: "longer description here?",
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
Parameters: []*spec3.Parameter{
|
||||
{ParameterProps: spec3.ParameterProps{
|
||||
Name: "b",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("Root level handler (bbb)"))
|
||||
},
|
||||
},
|
||||
},
|
||||
Namespace: []grafanaapiserver.APIRouteHandler{
|
||||
{
|
||||
Path: "/ccc",
|
||||
Spec: &spec3.PathProps{
|
||||
Summary: "an example at the root level",
|
||||
Description: "longer description here?",
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
Parameters: []*spec3.Parameter{
|
||||
{ParameterProps: spec3.ParameterProps{
|
||||
Name: "a",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
info, ok := request.RequestInfoFrom(r.Context())
|
||||
if !ok {
|
||||
responsewriters.ErrorNegotiated(
|
||||
apierrors.NewInternalError(fmt.Errorf("no RequestInfo found in the context")),
|
||||
b.codecs, schema.GroupVersion{}, w, r,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte("Custom namespace route ccc: " + info.Namespace))
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: VersionID}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
// SchemeBuilder points to a list of functions added to Scheme.
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
// AddToScheme is a common registration function for mapping packaged scoped group & version keys to a scheme.
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&RuntimeInfo{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
69
pkg/apis/example/v0alpha1/storage.go
Normal file
69
pkg/apis/example/v0alpha1/storage.go
Normal file
@ -0,0 +1,69 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Scoper = (*staticStorage)(nil)
|
||||
_ rest.SingularNameProvider = (*staticStorage)(nil)
|
||||
_ rest.Lister = (*staticStorage)(nil)
|
||||
_ rest.Storage = (*staticStorage)(nil)
|
||||
)
|
||||
|
||||
type staticStorage struct {
|
||||
info RuntimeInfo
|
||||
}
|
||||
|
||||
func newDeploymentInfoStorage() *staticStorage {
|
||||
return &staticStorage{
|
||||
info: RuntimeInfo{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: APIVersion,
|
||||
Kind: "DeploymentInfo",
|
||||
},
|
||||
BuildVersion: setting.BuildVersion,
|
||||
BuildCommit: setting.BuildCommit,
|
||||
BuildBranch: setting.BuildBranch,
|
||||
EnterpriseBuildCommit: setting.EnterpriseBuildCommit,
|
||||
BuildStamp: setting.BuildStamp,
|
||||
IsEnterprise: setting.IsEnterprise,
|
||||
Packaging: setting.Packaging,
|
||||
StartupTime: time.Now().UnixMilli(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticStorage) New() runtime.Object {
|
||||
return &RuntimeInfo{}
|
||||
}
|
||||
|
||||
func (s *staticStorage) Destroy() {}
|
||||
|
||||
func (s *staticStorage) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *staticStorage) GetSingularName() string {
|
||||
return "runtime"
|
||||
}
|
||||
|
||||
func (s *staticStorage) NewList() runtime.Object {
|
||||
return &RuntimeInfo{}
|
||||
}
|
||||
|
||||
func (s *staticStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return rest.NewDefaultTableConvertor(Resource("runtime")).ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *staticStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return &s.info, nil
|
||||
}
|
20
pkg/apis/example/v0alpha1/types.go
Normal file
20
pkg/apis/example/v0alpha1/types.go
Normal file
@ -0,0 +1,20 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type RuntimeInfo struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
StartupTime int64 `json:"startupTime,omitempty"`
|
||||
|
||||
BuildVersion string `json:"buildVersion,omitempty"`
|
||||
BuildCommit string `json:"buildCommit,omitempty"`
|
||||
EnterpriseBuildCommit string `json:"enterpriseBuildCommit,omitempty"`
|
||||
BuildBranch string `json:"buildBranch,omitempty"`
|
||||
BuildStamp int64 `json:"buildStamp,omitempty"`
|
||||
IsEnterprise bool `json:"enterprise,omitempty"`
|
||||
Packaging string `json:"packaging,omitempty"`
|
||||
}
|
37
pkg/apis/example/v0alpha1/zz_generated.deepcopy.go
Normal file
37
pkg/apis/example/v0alpha1/zz_generated.deepcopy.go
Normal file
@ -0,0 +1,37 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RuntimeInfo) DeepCopyInto(out *RuntimeInfo) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeInfo.
|
||||
func (in *RuntimeInfo) DeepCopy() *RuntimeInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RuntimeInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RuntimeInfo) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
@ -9,7 +9,6 @@ import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||
grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest"
|
||||
@ -36,6 +35,10 @@ func RegisterAPIService(p playlist.Service, apiregistration grafanaapiserver.API
|
||||
return builder
|
||||
}
|
||||
|
||||
func (b *PlaylistAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return SchemeGroupVersion
|
||||
}
|
||||
|
||||
func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
err := AddToScheme(scheme)
|
||||
if err != nil {
|
||||
@ -72,9 +75,8 @@ func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinition
|
||||
return getOpenAPIDefinitions
|
||||
}
|
||||
|
||||
// Register additional routes with the server
|
||||
func (b *PlaylistAPIBuilder) GetOpenAPIPostProcessor() func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
return nil
|
||||
func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
|
||||
return nil // no custom API routes
|
||||
}
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
|
@ -3,9 +3,13 @@ package apis
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
examplev0alpha1 "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
|
||||
playlistsv0alpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
|
||||
)
|
||||
|
||||
// WireSet is the list of all services
|
||||
// NOTE: you must also register the service in: pkg/registry/apis/apis.go
|
||||
var WireSet = wire.NewSet(
|
||||
playlistsv0alpha1.RegisterAPIService,
|
||||
examplev0alpha1.RegisterAPIService,
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ package apiregistry
|
||||
import (
|
||||
"context"
|
||||
|
||||
examplev0alpha1 "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
|
||||
playlistsv0alpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
)
|
||||
@ -13,8 +14,11 @@ var (
|
||||
|
||||
type Service struct{}
|
||||
|
||||
// ProvideService 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 ProvideService(
|
||||
_ *playlistsv0alpha1.PlaylistAPIBuilder,
|
||||
_ *examplev0alpha1.TestingAPIBuilder,
|
||||
) *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
@ -629,6 +629,13 @@ var (
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "grafanaAPIServerWithExperimentalAPIs",
|
||||
Description: "Register experimental APIs with the k8s API server",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "featureToggleAdminPage",
|
||||
Description: "Enable admin page for managing feature toggles from the Grafana front-end",
|
||||
|
@ -90,6 +90,7 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false
|
||||
traceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true
|
||||
metricsSummary,experimental,@grafana/observability-traces-and-profiling,false,false,false,true
|
||||
grafanaAPIServer,experimental,@grafana/grafana-app-platform-squad,false,false,false,false
|
||||
grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,false,false,false,false
|
||||
featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,false,true,false
|
||||
awsAsyncQueryCaching,preview,@grafana/aws-datasources,false,false,false,false
|
||||
splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false
|
||||
|
|
@ -371,6 +371,10 @@ const (
|
||||
// Enable Kubernetes API Server for Grafana resources
|
||||
FlagGrafanaAPIServer = "grafanaAPIServer"
|
||||
|
||||
// FlagGrafanaAPIServerWithExperimentalAPIs
|
||||
// Register experimental APIs with the k8s API server
|
||||
FlagGrafanaAPIServerWithExperimentalAPIs = "grafanaAPIServerWithExperimentalAPIs"
|
||||
|
||||
// FlagFeatureToggleAdminPage
|
||||
// Enable admin page for managing feature toggles from the Grafana front-end
|
||||
FlagFeatureToggleAdminPage = "featureToggleAdminPage"
|
||||
|
@ -1,7 +1,10 @@
|
||||
package grafanaapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
@ -12,6 +15,9 @@ import (
|
||||
// TODO: this (or something like it) belongs in grafana-app-sdk,
|
||||
// but lets keep it here while we iterate on a few simple examples
|
||||
type APIGroupBuilder interface {
|
||||
// Get the main group name
|
||||
GetGroupVersion() schema.GroupVersion
|
||||
|
||||
// Add the kinds to the server scheme
|
||||
InstallSchema(scheme *runtime.Scheme) error
|
||||
|
||||
@ -25,6 +31,27 @@ type APIGroupBuilder interface {
|
||||
// Get OpenAPI definitions
|
||||
GetOpenAPIDefinitions() common.GetOpenAPIDefinitions
|
||||
|
||||
// Register additional routes with the server
|
||||
GetOpenAPIPostProcessor() func(*spec3.OpenAPI) (*spec3.OpenAPI, error)
|
||||
// Get the API routes for each version
|
||||
GetAPIRoutes() *APIRoutes
|
||||
}
|
||||
|
||||
// This is used to implement dynamic sub-resources like pods/x/logs
|
||||
type APIRouteHandler struct {
|
||||
Path string // added to the appropriate level
|
||||
Spec *spec3.PathProps // Exposed in the open api service discovery
|
||||
Handler http.HandlerFunc // when Level = resource, the resource will be available in context
|
||||
}
|
||||
|
||||
// APIRoutes define the
|
||||
type APIRoutes struct {
|
||||
// Root handlers are registered directly after the apiVersion identifier
|
||||
Root []APIRouteHandler
|
||||
|
||||
// Namespace handlers are mounted under the namespace
|
||||
Namespace []APIRouteHandler
|
||||
|
||||
// Resource routes behave the same as pod/logs
|
||||
// it looks like a sub-resource, however the response is backed directly by an http handler
|
||||
// The current resource can be fetched through context
|
||||
Resource map[string]APIRouteHandler
|
||||
}
|
||||
|
198
pkg/services/grafana-apiserver/request_handler.go
Normal file
198
pkg/services/grafana-apiserver/request_handler.go
Normal file
@ -0,0 +1,198 @@
|
||||
package grafanaapiserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
)
|
||||
|
||||
type requestHandler struct {
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func getAPIHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder) (http.Handler, error) {
|
||||
useful := false // only true if any routes exist anywhere
|
||||
router := mux.NewRouter()
|
||||
var err error
|
||||
|
||||
for _, builder := range builders {
|
||||
routes := builder.GetAPIRoutes()
|
||||
if routes == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
gv := builder.GetGroupVersion()
|
||||
prefix := "/apis/" + gv.String()
|
||||
|
||||
// Root handlers
|
||||
var sub *mux.Router
|
||||
for _, route := range routes.Root {
|
||||
err = validPath(route.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sub == nil {
|
||||
sub = router.PathPrefix(prefix).Subrouter()
|
||||
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
|
||||
}
|
||||
|
||||
useful = true
|
||||
methods, err := methodsFromSpec(route.Path, route.Spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.HandleFunc(route.Path, route.Handler).
|
||||
Methods(methods...)
|
||||
}
|
||||
|
||||
// Namespace handlers
|
||||
sub = nil
|
||||
prefix += "/namespaces/{namespace}"
|
||||
for _, route := range routes.Namespace {
|
||||
err = validPath(route.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sub == nil {
|
||||
sub = router.PathPrefix(prefix).Subrouter()
|
||||
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
|
||||
}
|
||||
|
||||
useful = true
|
||||
methods, err := methodsFromSpec(route.Path, route.Spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.HandleFunc(route.Path, route.Handler).
|
||||
Methods(methods...)
|
||||
}
|
||||
|
||||
// getter := makeGetter(restConfig)
|
||||
for resource, route := range routes.Resource {
|
||||
err = validPath(route.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("TODO: %s/%v\n", resource, route)
|
||||
|
||||
// get a client for that resource kind
|
||||
//getter := makeGetter(restConfig)
|
||||
|
||||
useful = true
|
||||
// sub.HandleFunc(v.Slug, SubresourceHandlerWrapper(v.Handler, getter)).
|
||||
// Methods(methods...)
|
||||
}
|
||||
}
|
||||
|
||||
if !useful {
|
||||
return delegateHandler, nil
|
||||
}
|
||||
|
||||
// Per Gorilla Mux issue here: https://github.com/gorilla/mux/issues/616#issuecomment-798807509
|
||||
// default handler must come last
|
||||
router.PathPrefix("/").Handler(delegateHandler)
|
||||
|
||||
return &requestHandler{
|
||||
router: router,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The registered path must start with a slash, and (for now) not have any more
|
||||
func validPath(p string) error {
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
return fmt.Errorf("path must start with slash")
|
||||
}
|
||||
if strings.Count(p, "/") > 1 {
|
||||
return fmt.Errorf("path can only have one slash (for now)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *requestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.router.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func methodsFromSpec(slug string, props *spec3.PathProps) ([]string, error) {
|
||||
if props == nil {
|
||||
return []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, nil
|
||||
}
|
||||
|
||||
methods := make([]string, 0)
|
||||
if props.Get != nil {
|
||||
methods = append(methods, "GET")
|
||||
}
|
||||
if props.Post != nil {
|
||||
methods = append(methods, "POST")
|
||||
}
|
||||
if props.Put != nil {
|
||||
methods = append(methods, "PUT")
|
||||
}
|
||||
if props.Patch != nil {
|
||||
methods = append(methods, "PATCH")
|
||||
}
|
||||
if props.Delete != nil {
|
||||
methods = append(methods, "DELETE")
|
||||
}
|
||||
|
||||
if len(methods) == 0 {
|
||||
return nil, fmt.Errorf("invalid OpenAPI Spec for slug=%s without any methods in PathProps", slug)
|
||||
}
|
||||
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
type methodNotAllowedHandler struct{}
|
||||
|
||||
func (h *methodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(405) // method not allowed
|
||||
}
|
||||
|
||||
// Modify the 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
|
||||
func getOpenAPIPostProcessor(builders []APIGroupBuilder) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
if s.Paths == nil {
|
||||
return s, nil
|
||||
}
|
||||
for _, builder := range builders {
|
||||
routes := builder.GetAPIRoutes()
|
||||
if routes == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
gv := builder.GetGroupVersion()
|
||||
prefix := "/apis/" + gv.String()
|
||||
if s.Paths.Paths[prefix] != nil {
|
||||
copy := *s // will copy the rest of the properties
|
||||
copy.Info.Title = "Grafana API server: " + gv.Group
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
for resource, route := range routes.Resource {
|
||||
copy.Paths.Paths[prefix+"/namespaces/{namespace}/"+resource+"{name}"+route.Path] = &spec3.Path{
|
||||
PathProps: *route.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
return ©, nil
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
@ -3,7 +3,9 @@ package grafanaapiserver
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
@ -248,7 +250,23 @@ func (s *service) start(ctx context.Context) error {
|
||||
openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(defsGetter),
|
||||
openapinamer.NewDefinitionNamer(Scheme, scheme.Scheme))
|
||||
|
||||
// Add the custom routes to service discovery
|
||||
serverConfig.OpenAPIV3Config.PostProcessSpec3 = getOpenAPIPostProcessor(builders)
|
||||
|
||||
serverConfig.SkipOpenAPIInstallation = false
|
||||
serverConfig.BuildHandlerChainFunc = func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
|
||||
// Call DefaultBuildHandlerChain on the main entrypoint http.Handler
|
||||
// See https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/server/config.go#L906
|
||||
// DefaultBuildHandlerChain provides many things, notably CORS, HSTS, cache-control, authz and latency tracking
|
||||
requestHandler, err := getAPIHandler(
|
||||
delegateHandler,
|
||||
c.LoopbackClientConfig,
|
||||
builders)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not build handler chain func: %s", err.Error()))
|
||||
}
|
||||
return genericapiserver.DefaultBuildHandlerChain(requestHandler, c)
|
||||
}
|
||||
|
||||
// Create the server
|
||||
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegate())
|
||||
|
Loading…
Reference in New Issue
Block a user