mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PoC: Define userstorage API (#95557)
This commit is contained in:
parent
c3571752b6
commit
c3494614e3
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -72,6 +72,7 @@
|
|||||||
/pkg/apis/ @grafana/grafana-app-platform-squad
|
/pkg/apis/ @grafana/grafana-app-platform-squad
|
||||||
/pkg/apis/alerting_notifications @grafana/grafana-app-platform-squad @grafana/alerting-backend @grafana/alerting-frontend
|
/pkg/apis/alerting_notifications @grafana/grafana-app-platform-squad @grafana/alerting-backend @grafana/alerting-frontend
|
||||||
/pkg/apis/query @grafana/grafana-datasources-core-services
|
/pkg/apis/query @grafana/grafana-datasources-core-services
|
||||||
|
/pkg/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend
|
||||||
/pkg/bus/ @grafana/grafana-search-and-storage
|
/pkg/bus/ @grafana/grafana-search-and-storage
|
||||||
/pkg/cmd/ @grafana/grafana-backend-group
|
/pkg/cmd/ @grafana/grafana-backend-group
|
||||||
/pkg/cmd/grafana-cli/commands/install_command.go @grafana/plugins-platform-backend
|
/pkg/cmd/grafana-cli/commands/install_command.go @grafana/plugins-platform-backend
|
||||||
@ -676,6 +677,7 @@ embed.go @grafana/grafana-as-code
|
|||||||
/pkg/registry/apis/ @grafana/grafana-app-platform-squad
|
/pkg/registry/apis/ @grafana/grafana-app-platform-squad
|
||||||
/pkg/registry/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
|
/pkg/registry/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
|
||||||
/pkg/registry/apis/query @grafana/grafana-datasources-core-services
|
/pkg/registry/apis/query @grafana/grafana-datasources-core-services
|
||||||
|
/pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend
|
||||||
/pkg/codegen/ @grafana/grafana-as-code
|
/pkg/codegen/ @grafana/grafana-as-code
|
||||||
/pkg/codegen/generators @grafana/grafana-as-code
|
/pkg/codegen/generators @grafana/grafana-as-code
|
||||||
/pkg/kinds/*/*_gen.go @grafana/grafana-as-code
|
/pkg/kinds/*/*_gen.go @grafana/grafana-as-code
|
||||||
|
@ -215,6 +215,7 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage |
|
| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage |
|
||||||
| `timeRangeProvider` | Enables time pickers sync |
|
| `timeRangeProvider` | Enables time pickers sync |
|
||||||
| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor |
|
| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor |
|
||||||
|
| `userStorageAPI` | Enables the user storage API |
|
||||||
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins |
|
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins |
|
||||||
| `dashboardSchemaV2` | Enables the new dashboard schema version 2, implementing changes necessary for dynamic dashboards and dashboards as code. |
|
| `dashboardSchemaV2` | Enables the new dashboard schema version 2, implementing changes necessary for dynamic dashboards and dashboards as code. |
|
||||||
| `playlistsWatcher` | Enables experimental watcher for playlists |
|
| `playlistsWatcher` | Enables experimental watcher for playlists |
|
||||||
|
@ -227,6 +227,7 @@ export interface FeatureToggles {
|
|||||||
unifiedStorageBigObjectsSupport?: boolean;
|
unifiedStorageBigObjectsSupport?: boolean;
|
||||||
timeRangeProvider?: boolean;
|
timeRangeProvider?: boolean;
|
||||||
prometheusUsesCombobox?: boolean;
|
prometheusUsesCombobox?: boolean;
|
||||||
|
userStorageAPI?: boolean;
|
||||||
azureMonitorDisableLogLimit?: boolean;
|
azureMonitorDisableLogLimit?: boolean;
|
||||||
preinstallAutoUpdate?: boolean;
|
preinstallAutoUpdate?: boolean;
|
||||||
dashboardSchemaV2?: boolean;
|
dashboardSchemaV2?: boolean;
|
||||||
|
6
pkg/apis/userstorage/v0alpha1/doc.go
Normal file
6
pkg/apis/userstorage/v0alpha1/doc.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// +k8s:deepcopy-gen=package
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
// +k8s:defaulter-gen=TypeMeta
|
||||||
|
// +groupName=userstorage.grafana.com
|
||||||
|
|
||||||
|
package v0alpha1
|
71
pkg/apis/userstorage/v0alpha1/register.go
Normal file
71
pkg/apis/userstorage/v0alpha1/register.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package v0alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GROUP = "userstorage.grafana.app"
|
||||||
|
VERSION = "v0alpha1"
|
||||||
|
APIVERSION = GROUP + "/" + VERSION
|
||||||
|
)
|
||||||
|
|
||||||
|
var UserStorageResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||||
|
"user-storage", "user-storage", "UserStorage",
|
||||||
|
func() runtime.Object { return &UserStorage{} },
|
||||||
|
func() runtime.Object { return &UserStorageList{} },
|
||||||
|
utils.TableColumns{
|
||||||
|
Definition: []metav1.TableColumnDefinition{
|
||||||
|
{Name: "Name", Type: "string", Format: "name"},
|
||||||
|
{Name: "Data", Type: "string"},
|
||||||
|
{Name: "Created At", Type: "date"},
|
||||||
|
},
|
||||||
|
Reader: func(obj any) ([]interface{}, error) {
|
||||||
|
m, ok := obj.(*UserStorage)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("plugin-storage")
|
||||||
|
}
|
||||||
|
return []interface{}{
|
||||||
|
m.Name,
|
||||||
|
m.Spec.Data,
|
||||||
|
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, // default table converter
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
|
||||||
|
|
||||||
|
// SchemeBuilder is used by standard codegen
|
||||||
|
SchemeBuilder runtime.SchemeBuilder
|
||||||
|
localSchemeBuilder = &SchemeBuilder
|
||||||
|
AddToScheme = localSchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
localSchemeBuilder.Register(addKnownTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds the list of known types to the given scheme.
|
||||||
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||||
|
&UserStorage{},
|
||||||
|
&UserStorageList{},
|
||||||
|
)
|
||||||
|
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||||
|
func Resource(resource string) schema.GroupResource {
|
||||||
|
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||||
|
}
|
26
pkg/apis/userstorage/v0alpha1/types.go
Normal file
26
pkg/apis/userstorage/v0alpha1/types.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package v0alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
type UserStorage struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec UserStorageSpec `json:"spec,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserStorageSpec struct {
|
||||||
|
// Data is the key:value stored in the user storage for a service.
|
||||||
|
Data map[string]string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
type UserStorageList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Items []UserStorage `json:"items,omitempty"`
|
||||||
|
}
|
95
pkg/apis/userstorage/v0alpha1/zz_generated.deepcopy.go
Normal file
95
pkg/apis/userstorage/v0alpha1/zz_generated.deepcopy.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//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 *UserStorage) DeepCopyInto(out *UserStorage) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStorage.
|
||||||
|
func (in *UserStorage) DeepCopy() *UserStorage {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(UserStorage)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *UserStorage) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *UserStorageList) DeepCopyInto(out *UserStorageList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]UserStorage, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStorageList.
|
||||||
|
func (in *UserStorageList) DeepCopy() *UserStorageList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(UserStorageList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *UserStorageList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *UserStorageSpec) DeepCopyInto(out *UserStorageSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Data != nil {
|
||||||
|
in, out := &in.Data, &out.Data
|
||||||
|
*out = make(map[string]string, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
(*out)[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStorageSpec.
|
||||||
|
func (in *UserStorageSpec) DeepCopy() *UserStorageSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(UserStorageSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
19
pkg/apis/userstorage/v0alpha1/zz_generated.defaults.go
Normal file
19
pkg/apis/userstorage/v0alpha1/zz_generated.defaults.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//go:build !ignore_autogenerated
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package v0alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterDefaults adds defaulters functions to the given scheme.
|
||||||
|
// Public to allow building arbitrary schemes.
|
||||||
|
// All generated defaulters are covering - they call all nested defaulters.
|
||||||
|
func RegisterDefaults(scheme *runtime.Scheme) error {
|
||||||
|
return nil
|
||||||
|
}
|
137
pkg/apis/userstorage/v0alpha1/zz_generated.openapi.go
Normal file
137
pkg/apis/userstorage/v0alpha1/zz_generated.openapi.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
//go:build !ignore_autogenerated
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Code generated by openapi-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package v0alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
common "k8s.io/kube-openapi/pkg/common"
|
||||||
|
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||||
|
return map[string]common.OpenAPIDefinition{
|
||||||
|
"github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorage": schema_pkg_apis_userstorage_v0alpha1_UserStorage(ref),
|
||||||
|
"github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorageList": schema_pkg_apis_userstorage_v0alpha1_UserStorageList(ref),
|
||||||
|
"github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorageSpec": schema_pkg_apis_userstorage_v0alpha1_UserStorageSpec(ref),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func schema_pkg_apis_userstorage_v0alpha1_UserStorage(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: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorageSpec"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: []string{
|
||||||
|
"github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorageSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func schema_pkg_apis_userstorage_v0alpha1_UserStorageList(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: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: []string{"array"},
|
||||||
|
Items: &spec.SchemaOrArray{
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorage"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: []string{
|
||||||
|
"github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1.UserStorage", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func schema_pkg_apis_userstorage_v0alpha1_UserStorageSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||||
|
return common.OpenAPIDefinition{
|
||||||
|
Schema: spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: []string{"object"},
|
||||||
|
Properties: map[string]spec.Schema{
|
||||||
|
"data": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Data is the key:value stored in the user storage for a service.",
|
||||||
|
Type: []string{"object"},
|
||||||
|
AdditionalProperties: &spec.SchemaOrBool{
|
||||||
|
Allows: true,
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: "",
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"data"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/scope"
|
"github.com/grafana/grafana/pkg/registry/apis/scope"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/search"
|
"github.com/grafana/grafana/pkg/registry/apis/search"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/userstorage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct{}
|
type Service struct{}
|
||||||
@ -30,6 +31,7 @@ func ProvideRegistryServiceSink(
|
|||||||
_ *query.QueryAPIBuilder,
|
_ *query.QueryAPIBuilder,
|
||||||
_ *notifications.NotificationsAPIBuilder,
|
_ *notifications.NotificationsAPIBuilder,
|
||||||
_ *search.SearchAPIBuilder,
|
_ *search.SearchAPIBuilder,
|
||||||
|
_ *userstorage.UserStorageAPIBuilder,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{}
|
return &Service{}
|
||||||
}
|
}
|
||||||
|
118
pkg/registry/apis/userstorage/register.go
Normal file
118
pkg/registry/apis/userstorage/register.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package userstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
|
"k8s.io/kube-openapi/pkg/common"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
userstorage "github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ builder.APIGroupBuilder = (*UserStorageAPIBuilder)(nil)
|
||||||
|
|
||||||
|
type UserStorageAPIBuilder struct {
|
||||||
|
registerer prometheus.Registerer
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, registerer prometheus.Registerer) *UserStorageAPIBuilder {
|
||||||
|
if !features.IsEnabledGlobally(featuremgmt.FlagUserStorageAPI) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := &UserStorageAPIBuilder{
|
||||||
|
registerer: registerer,
|
||||||
|
}
|
||||||
|
apiregistration.RegisterAPI(builder)
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UserStorageAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||||
|
return userstorage.SchemeGroupVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UserStorageAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||||
|
gv := userstorage.SchemeGroupVersion
|
||||||
|
err := userstorage.AddToScheme(scheme)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link this version to the internal representation.
|
||||||
|
// This is used for server-side-apply (PATCH), and avoids the error:
|
||||||
|
// "no kind is registered for the type"
|
||||||
|
// addKnownTypes(scheme, schema.GroupVersion{
|
||||||
|
// Group: userstorage.GROUP,
|
||||||
|
// Version: runtime.APIVersionInternal,
|
||||||
|
// })
|
||||||
|
metav1.AddToGroupVersion(scheme, gv)
|
||||||
|
return scheme.SetVersionPriority(gv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UserStorageAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||||
|
resourceInfo := userstorage.UserStorageResourceInfo
|
||||||
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
|
storageReg, err := newStorage(opts.Scheme, opts.OptsGetter, b.registerer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
storage[resourceInfo.StoragePath()] = storageReg
|
||||||
|
|
||||||
|
apiGroupInfo.VersionedResourcesStorageMap[userstorage.VERSION] = storage
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UserStorageAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||||
|
return userstorage.GetOpenAPIDefinitions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UserStorageAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UserStorageAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||||
|
return authorizer.AuthorizerFunc(
|
||||||
|
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||||
|
if !attr.IsResourceRequest() {
|
||||||
|
return authorizer.DecisionNoOpinion, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// require a user
|
||||||
|
u, err := identity.GetRequester(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return authorizer.DecisionDeny, "valid user is required", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if is admin
|
||||||
|
if u.GetIsGrafanaAdmin() {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch attr.GetVerb() {
|
||||||
|
case "create":
|
||||||
|
// Create requests are validated later since we don't have access to the resource name
|
||||||
|
return authorizer.DecisionNoOpinion, "", nil
|
||||||
|
case "get", "delete", "patch", "update":
|
||||||
|
// Only allow the user to access their own settings
|
||||||
|
if !compareResourceNameAndUserUID(attr.GetName(), u) {
|
||||||
|
return authorizer.DecisionDeny, "forbidden", nil
|
||||||
|
}
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
default:
|
||||||
|
// Forbid the rest
|
||||||
|
return authorizer.DecisionDeny, "forbidden", nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
93
pkg/registry/apis/userstorage/register_test.go
Normal file
93
pkg/registry/apis/userstorage/register_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package userstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthorizer(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
requesterID string
|
||||||
|
verb string
|
||||||
|
objectName string
|
||||||
|
decision authorizer.Decision
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid authorization",
|
||||||
|
requesterID: "123",
|
||||||
|
objectName: "user:123",
|
||||||
|
verb: "get",
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user",
|
||||||
|
requesterID: "123",
|
||||||
|
objectName: "user:456",
|
||||||
|
verb: "get",
|
||||||
|
decision: authorizer.DecisionDeny,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "admin user",
|
||||||
|
requesterID: "admin",
|
||||||
|
objectName: "",
|
||||||
|
verb: "list",
|
||||||
|
decision: authorizer.DecisionAllow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create request",
|
||||||
|
requesterID: "123",
|
||||||
|
objectName: "",
|
||||||
|
verb: "create",
|
||||||
|
decision: authorizer.DecisionNoOpinion,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbidden action",
|
||||||
|
requesterID: "123",
|
||||||
|
objectName: "",
|
||||||
|
verb: "list",
|
||||||
|
decision: authorizer.DecisionDeny,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
requester := &identity.StaticRequester{Type: "user", UserUID: tt.requesterID}
|
||||||
|
if tt.requesterID == "admin" {
|
||||||
|
requester.IsGrafanaAdmin = true
|
||||||
|
}
|
||||||
|
ctx := identity.WithRequester(context.Background(), requester)
|
||||||
|
apiBuilder := &UserStorageAPIBuilder{}
|
||||||
|
auth := apiBuilder.GetAuthorizer()
|
||||||
|
at := &fakeAttributes{
|
||||||
|
verb: tt.verb,
|
||||||
|
name: tt.objectName,
|
||||||
|
}
|
||||||
|
decision, _, err := auth.Authorize(ctx, at)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.decision, decision)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeAttributes struct {
|
||||||
|
authorizer.Attributes
|
||||||
|
verb string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a fakeAttributes) GetVerb() string {
|
||||||
|
return a.verb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a fakeAttributes) IsResourceRequest() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a fakeAttributes) GetName() string {
|
||||||
|
return a.name
|
||||||
|
}
|
43
pkg/registry/apis/userstorage/storage.go
Normal file
43
pkg/registry/apis/userstorage/storage.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package userstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
|
||||||
|
userstorage "github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1"
|
||||||
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||||
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ grafanarest.Storage = (*storage)(nil)
|
||||||
|
|
||||||
|
type storage struct {
|
||||||
|
*genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, registerer prometheus.Registerer) (*storage, error) {
|
||||||
|
resourceInfo := userstorage.UserStorageResourceInfo
|
||||||
|
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
|
||||||
|
storageStrategy := newStrategy(scheme, resourceInfo.GroupVersion(), registerer)
|
||||||
|
|
||||||
|
store := &genericregistry.Store{
|
||||||
|
NewFunc: resourceInfo.NewFunc,
|
||||||
|
NewListFunc: resourceInfo.NewListFunc,
|
||||||
|
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
|
||||||
|
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
|
||||||
|
PredicateFunc: grafanaregistry.Matcher,
|
||||||
|
DefaultQualifiedResource: resourceInfo.GroupResource(),
|
||||||
|
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
|
||||||
|
TableConvertor: resourceInfo.TableConverter(),
|
||||||
|
CreateStrategy: storageStrategy,
|
||||||
|
UpdateStrategy: storageStrategy,
|
||||||
|
DeleteStrategy: strategy,
|
||||||
|
}
|
||||||
|
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
|
||||||
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &storage{Store: store}, nil
|
||||||
|
}
|
131
pkg/registry/apis/userstorage/strategy.go
Normal file
131
pkg/registry/apis/userstorage/strategy.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package userstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Target for user storage size < 3MB
|
||||||
|
userstorageSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Namespace: "userstorage",
|
||||||
|
Name: "object_size_bytes",
|
||||||
|
Help: "Histogram of user storage object sizes in bytes, broken down by service name",
|
||||||
|
Buckets: prometheus.ExponentialBucketsRange(1024, 8*1024*1024, 8), // From 1 KB to 8 MB
|
||||||
|
}, []string{"service"})
|
||||||
|
)
|
||||||
|
|
||||||
|
type genericStrategy interface {
|
||||||
|
rest.RESTCreateStrategy
|
||||||
|
rest.RESTUpdateStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
type userstorageStrategy struct {
|
||||||
|
genericStrategy
|
||||||
|
|
||||||
|
registerer prometheus.Registerer
|
||||||
|
}
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func newStrategy(typer runtime.ObjectTyper, gv schema.GroupVersion, registerer prometheus.Registerer) *userstorageStrategy {
|
||||||
|
once.Do(func() {
|
||||||
|
if registerer != nil {
|
||||||
|
registerer.MustRegister(
|
||||||
|
userstorageSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
genericStrategy := grafanaregistry.NewStrategy(typer, gv)
|
||||||
|
return &userstorageStrategy{genericStrategy, registerer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareResourceNameAndUserUID(name string, u identity.Requester) bool {
|
||||||
|
parsedName, err := parseName(name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// u.GetUID() returns user:<user_uid> so we need to remove the user: prefix
|
||||||
|
userUID := strings.Split(u.GetUID(), ":")
|
||||||
|
if len(userUID) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedName.UID == userUID[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerSize(obj runtime.Object) {
|
||||||
|
meta, err := utils.MetaAccessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedName, err := parseName(meta.GetName())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
if err := json.NewEncoder(b).Encode(obj); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userstorageSize.WithLabelValues(parsedName.Service).Observe(float64(b.Len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ensures that when creating a userstorage object, the name matches the user id.
|
||||||
|
func (g *userstorageStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
|
u, err := identity.GetRequester(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return field.ErrorList{field.InternalError(nil, fmt.Errorf("failed to get requester: %v", err))}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := utils.MetaAccessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return field.ErrorList{field.InternalError(nil, fmt.Errorf("failed to get meta accessor: %v", err))}
|
||||||
|
}
|
||||||
|
|
||||||
|
nameMatch := compareResourceNameAndUserUID(meta.GetName(), u)
|
||||||
|
if !nameMatch {
|
||||||
|
return field.ErrorList{field.Forbidden(field.NewPath("metadata").Child("name"), "name must match service:user_uid")}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSize(obj)
|
||||||
|
return field.ErrorList{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *userstorageStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
registerSize(obj)
|
||||||
|
return field.ErrorList{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageObjectName struct {
|
||||||
|
Service string
|
||||||
|
UID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseName(name string) (*storageObjectName, error) {
|
||||||
|
vals := strings.Split(name, ":")
|
||||||
|
if len(vals) != 2 {
|
||||||
|
return nil, errors.New("name must be in the format <service>:<user_uid>")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &storageObjectName{
|
||||||
|
Service: vals[0],
|
||||||
|
UID: vals[1],
|
||||||
|
}, nil
|
||||||
|
}
|
56
pkg/registry/apis/userstorage/strategy_test.go
Normal file
56
pkg/registry/apis/userstorage/strategy_test.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package userstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
|
"github.com/grafana/grafana/pkg/apis/userstorage/v0alpha1"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
requesterID string
|
||||||
|
objectName string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid userstorage object",
|
||||||
|
requesterID: "123",
|
||||||
|
objectName: "basic-panel:123",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid userstorage object",
|
||||||
|
requesterID: "123",
|
||||||
|
objectName: "basic-panel:456",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
requester := &identity.StaticRequester{Type: "user", UserUID: tt.requesterID}
|
||||||
|
obj := &v0alpha1.UserStorage{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Name: tt.objectName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := identity.WithRequester(context.Background(), requester)
|
||||||
|
|
||||||
|
strategy := newStrategy(nil, schema.GroupVersion{}, prometheus.DefaultRegisterer)
|
||||||
|
errs := strategy.Validate(ctx, obj)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.NotEmpty(t, errs)
|
||||||
|
} else {
|
||||||
|
assert.Empty(t, errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/registry/apis/scope"
|
"github.com/grafana/grafana/pkg/registry/apis/scope"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/search"
|
"github.com/grafana/grafana/pkg/registry/apis/search"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/service"
|
"github.com/grafana/grafana/pkg/registry/apis/service"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/userstorage"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,4 +41,5 @@ var WireSet = wire.NewSet(
|
|||||||
notifications.RegisterAPIService,
|
notifications.RegisterAPIService,
|
||||||
//sso.RegisterAPIService,
|
//sso.RegisterAPIService,
|
||||||
search.RegisterAPIService,
|
search.RegisterAPIService,
|
||||||
|
userstorage.RegisterAPIService,
|
||||||
)
|
)
|
||||||
|
@ -1567,6 +1567,12 @@ var (
|
|||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStageExperimental,
|
||||||
Owner: grafanaObservabilityMetricsSquad,
|
Owner: grafanaObservabilityMetricsSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "userStorageAPI",
|
||||||
|
Description: "Enables the user storage API",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaPluginsPlatformSquad,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "azureMonitorDisableLogLimit",
|
Name: "azureMonitorDisableLogLimit",
|
||||||
Description: "Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.",
|
Description: "Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.",
|
||||||
|
@ -208,6 +208,7 @@ pluginsSriChecks,experimental,@grafana/plugins-platform-backend,false,false,fals
|
|||||||
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
|
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
|
||||||
timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false
|
timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||||
prometheusUsesCombobox,experimental,@grafana/observability-metrics,false,false,false
|
prometheusUsesCombobox,experimental,@grafana/observability-metrics,false,false,false
|
||||||
|
userStorageAPI,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
||||||
preinstallAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
|
preinstallAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
dashboardSchemaV2,experimental,@grafana/dashboards-squad,false,false,true
|
dashboardSchemaV2,experimental,@grafana/dashboards-squad,false,false,true
|
||||||
|
|
@ -843,6 +843,10 @@ const (
|
|||||||
// Use new combobox component for Prometheus query editor
|
// Use new combobox component for Prometheus query editor
|
||||||
FlagPrometheusUsesCombobox = "prometheusUsesCombobox"
|
FlagPrometheusUsesCombobox = "prometheusUsesCombobox"
|
||||||
|
|
||||||
|
// FlagUserStorageAPI
|
||||||
|
// Enables the user storage API
|
||||||
|
FlagUserStorageAPI = "userStorageAPI"
|
||||||
|
|
||||||
// FlagAzureMonitorDisableLogLimit
|
// FlagAzureMonitorDisableLogLimit
|
||||||
// Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.
|
// Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.
|
||||||
FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit"
|
FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit"
|
||||||
|
@ -3390,6 +3390,18 @@
|
|||||||
"codeowner": "@grafana/identity-access-team"
|
"codeowner": "@grafana/identity-access-team"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "userStorageAPI",
|
||||||
|
"resourceVersion": "1730204321785",
|
||||||
|
"creationTimestamp": "2024-10-29T12:18:41Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Enables the user storage API",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/plugins-platform-backend"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "vizActions",
|
"name": "vizActions",
|
||||||
|
Loading…
Reference in New Issue
Block a user