Preferences: Add read-only APIServer for preferences and dashboard stars (#106109)

This commit is contained in:
Ryan McKinley
2025-08-28 19:51:32 +03:00
committed by GitHub
parent 05c52936a8
commit 43648d20c3
81 changed files with 3178 additions and 25 deletions
+1
View File
@@ -102,6 +102,7 @@ COPY apps/investigations apps/investigations
COPY apps/advisor apps/advisor
COPY apps/dashboard apps/dashboard
COPY apps/folder apps/folder
COPY apps/preferences apps/preferences
COPY apps/iam apps/iam
COPY apps apps
COPY kindsv2 kindsv2
+1 -1
View File
@@ -25,7 +25,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler
require (
cloud.google.com/go/compute/metadata v0.7.0 // indirect
dario.cat/mergo v1.0.1 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
+2 -2
View File
@@ -72,8 +72,8 @@ cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2zn
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+1 -1
View File
@@ -43,7 +43,7 @@ require (
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/storage v1.55.0 // indirect
cuelang.org/go v0.11.1 // indirect
dario.cat/mergo v1.0.1 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
+2 -2
View File
@@ -80,8 +80,8 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSB
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+5 -2
View File
@@ -4,7 +4,9 @@ go 1.24.6
require (
github.com/grafana/grafana-app-sdk v0.40.3
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
k8s.io/apimachinery v0.33.3
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
)
require (
@@ -34,11 +36,13 @@ require (
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
@@ -54,7 +58,6 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/client-go v0.33.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
+2
View File
@@ -36,6 +36,8 @@ github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhck
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+2 -1
View File
@@ -10,7 +10,8 @@ manifest: {
go: {enabled: true}
}
kinds: [
preferencesv1alpha1,
preferencesV1alpha1,
starsV1alpha1,
]
}
}
+7 -1
View File
@@ -1,10 +1,16 @@
package preferences
preferencesv1alpha1: {
preferencesV1alpha1: {
kind: "Preferences"
pluralName: "Preferences"
scope: "Namespaced"
validation: {
operations: [
"CREATE",
"UPDATE",
]
}
schema: {
spec: {
// UID for the home dashboard
+27
View File
@@ -0,0 +1,27 @@
package preferences
starsV1alpha1: {
kind: "Stars"
pluralName: "Stars"
scope: "Namespaced"
validation: {
operations: [
"CREATE",
"UPDATE",
]
}
schema: {
#Resource: {
group: string
kind: string
// The set of resources
// +listType=set
names: [...string]
}
spec: {
resource: [...#Resource]
}
}
}
@@ -0,0 +1,82 @@
package v1alpha1
import (
"fmt"
time "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
var PreferencesResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion,
"preferences", "preferences", "Preferences",
func() runtime.Object { return &Preferences{} },
func() runtime.Object { return &PreferencesList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]any, error) {
p, ok := obj.(*Preferences)
if ok && p != nil {
return []any{
p.Name,
p.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
return nil, fmt.Errorf("expected preferences")
},
},
)
var StarsResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion,
"stars", "stars", "Stars",
func() runtime.Object { return &Stars{} },
func() runtime.Object { return &StarsList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]any, error) {
p, ok := obj.(*Stars)
if ok && p != nil {
return []any{
p.Name,
p.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
return nil, fmt.Errorf("expected stars")
},
},
)
var (
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
schemeGroupVersion = GroupVersion
)
func init() {
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
}
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(schemeGroupVersion,
&Preferences{},
&PreferencesList{},
&Stars{},
&StarsList{},
)
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
return nil
}
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return nil // return RegisterDefaults(scheme)
}
@@ -0,0 +1,28 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1alpha1
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// StarsJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type StarsJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*StarsJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*StarsJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &StarsJSONCodec{}
@@ -0,0 +1,31 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type StarsMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewStarsMetadata creates a new StarsMetadata object.
func NewStarsMetadata() *StarsMetadata {
return &StarsMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}
@@ -0,0 +1,319 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1alpha1
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type Stars struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the Stars
Spec StarsSpec `json:"spec" yaml:"spec"`
Status StarsStatus `json:"status" yaml:"status"`
}
func (o *Stars) GetSpec() any {
return o.Spec
}
func (o *Stars) SetSpec(spec any) error {
cast, ok := spec.(StarsSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *Stars) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *Stars) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
default:
return nil, false
}
}
func (o *Stars) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(StarsStatus)
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type StarsStatus", value)
}
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *Stars) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *Stars) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *Stars) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *Stars) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *Stars) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *Stars) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *Stars) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *Stars) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *Stars) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *Stars) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *Stars) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *Stars) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *Stars) DeepCopy() *Stars {
cpy := &Stars{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *Stars) DeepCopyInto(dst *Stars) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &Stars{}
// +k8s:openapi-gen=true
type StarsList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []Stars `json:"items" yaml:"items"`
}
func (o *StarsList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *StarsList) Copy() resource.ListObject {
cpy := &StarsList{
TypeMeta: o.TypeMeta,
Items: make([]Stars, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*Stars); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *StarsList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *StarsList) SetItems(items []resource.Object) {
o.Items = make([]Stars, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*Stars)
}
}
func (o *StarsList) DeepCopy() *StarsList {
cpy := &StarsList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *StarsList) DeepCopyInto(dst *StarsList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &StarsList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *StarsSpec) DeepCopy() *StarsSpec {
cpy := &StarsSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *StarsSpec) DeepCopyInto(dst *StarsSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of StarsStatus
func (s *StarsStatus) DeepCopy() *StarsStatus {
cpy := &StarsStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies StarsStatus into another StarsStatus object
func (s *StarsStatus) DeepCopyInto(dst *StarsStatus) {
resource.CopyObjectInto(dst, s)
}
@@ -0,0 +1,34 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaStars = resource.NewSimpleSchema("preferences.grafana.app", "v1alpha1", &Stars{}, &StarsList{}, resource.WithKind("Stars"),
resource.WithPlural("stars"), resource.WithScope(resource.NamespacedScope))
kindStars = resource.Kind{
Schema: schemaStars,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &StarsJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func StarsKind() resource.Kind {
return kindStars
}
// Schema returns a resource.SimpleSchema representation of Stars
func StarsSchema() *resource.SimpleSchema {
return schemaStars
}
// Interface compliance checks
var _ resource.Schema = kindStars
@@ -0,0 +1,31 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// +k8s:openapi-gen=true
type StarsResource struct {
Group string `json:"group"`
Kind string `json:"kind"`
// The set of resources
// +listType=set
Names []string `json:"names"`
}
// NewStarsResource creates a new StarsResource object.
func NewStarsResource() *StarsResource {
return &StarsResource{
Names: []string{},
}
}
// +k8s:openapi-gen=true
type StarsSpec struct {
Resource []StarsResource `json:"resource"`
}
// NewStarsSpec creates a new StarsSpec object.
func NewStarsSpec() *StarsSpec {
return &StarsSpec{
Resource: []StarsResource{},
}
}
@@ -0,0 +1,44 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// +k8s:openapi-gen=true
type StarsstatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StarsStatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewStarsstatusOperatorState creates a new StarsstatusOperatorState object.
func NewStarsstatusOperatorState() *StarsstatusOperatorState {
return &StarsstatusOperatorState{}
}
// +k8s:openapi-gen=true
type StarsStatus struct {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StarsstatusOperatorState `json:"operatorStates,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewStarsStatus creates a new StarsStatus object.
func NewStarsStatus() *StarsStatus {
return &StarsStatus{}
}
// +k8s:openapi-gen=true
type StarsStatusOperatorStateState string
const (
StarsStatusOperatorStateStateSuccess StarsStatusOperatorStateState = "success"
StarsStatusOperatorStateStateInProgress StarsStatusOperatorStateState = "in_progress"
StarsStatusOperatorStateStateFailed StarsStatusOperatorStateState = "failed"
)
@@ -0,0 +1,639 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
package v1alpha1
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/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences": schema_pkg_apis_preferences_v1alpha1_Preferences(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences": schema_pkg_apis_preferences_v1alpha1_PreferencesCookiePreferences(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesList": schema_pkg_apis_preferences_v1alpha1_PreferencesList(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec": schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus": schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars": schema_pkg_apis_preferences_v1alpha1_Stars(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsList": schema_pkg_apis_preferences_v1alpha1_StarsList(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource": schema_pkg_apis_preferences_v1alpha1_StarsResource(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec": schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus": schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref),
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref),
}
}
func schema_pkg_apis_preferences_v1alpha1_Preferences(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{
Description: "Spec is the spec of the Preferences",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec"),
},
},
"status": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus"),
},
},
},
Required: []string{"metadata", "spec", "status"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesCookiePreferences(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"analytics": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
"performance": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
"functional": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesList(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/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences"),
},
},
},
},
},
},
Required: []string{"metadata", "items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"bookmarkUrls": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"bookmarkUrls"},
},
},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"homeTab": {
SchemaProps: spec.SchemaProps{
Description: "one of: '' | 'query' | 'starred';",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"homeDashboardUID": {
SchemaProps: spec.SchemaProps{
Description: "UID for the home dashboard",
Type: []string{"string"},
Format: "",
},
},
"timezone": {
SchemaProps: spec.SchemaProps{
Description: "The timezone selection",
Type: []string{"string"},
Format: "",
},
},
"weekStart": {
SchemaProps: spec.SchemaProps{
Description: "day of the week (sunday, monday, etc)",
Type: []string{"string"},
Format: "",
},
},
"theme": {
SchemaProps: spec.SchemaProps{
Description: "light, dark, empty is default",
Type: []string{"string"},
Format: "",
},
},
"language": {
SchemaProps: spec.SchemaProps{
Description: "Selected language (beta)",
Type: []string{"string"},
Format: "",
},
},
"regionalFormat": {
SchemaProps: spec.SchemaProps{
Description: "Selected locale (beta)",
Type: []string{"string"},
Format: "",
},
},
"queryHistory": {
SchemaProps: spec.SchemaProps{
Description: "Explore query history preferences",
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"),
},
},
"cookiePreferences": {
SchemaProps: spec.SchemaProps{
Description: "Cookie preferences",
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences"),
},
},
"navbar": {
SchemaProps: spec.SchemaProps{
Description: "Navigation preferences",
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"operatorStates": {
SchemaProps: spec.SchemaProps{
Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"),
},
},
},
},
},
"additionalFields": {
SchemaProps: spec.SchemaProps{
Description: "additionalFields is reserved for future use",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"},
}
}
func schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"lastEvaluation": {
SchemaProps: spec.SchemaProps{
Description: "lastEvaluation is the ResourceVersion last evaluated",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"state": {
SchemaProps: spec.SchemaProps{
Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"descriptiveState": {
SchemaProps: spec.SchemaProps{
Description: "descriptiveState is an optional more descriptive state field which has no requirements on format",
Type: []string{"string"},
Format: "",
},
},
"details": {
SchemaProps: spec.SchemaProps{
Description: "details contains any extra information that is operator-specific",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
Required: []string{"lastEvaluation", "state"},
},
},
}
}
func schema_pkg_apis_preferences_v1alpha1_Stars(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{
Description: "Spec is the spec of the Stars",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec"),
},
},
"status": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus"),
},
},
},
Required: []string{"metadata", "spec", "status"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_preferences_v1alpha1_StarsList(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/apps/preferences/pkg/apis/preferences/v1alpha1.Stars"),
},
},
},
},
},
},
Required: []string{"metadata", "items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_preferences_v1alpha1_StarsResource(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"group": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"kind": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"names": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "set",
},
},
SchemaProps: spec.SchemaProps{
Description: "The set of resources",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"group", "kind", "names"},
},
},
}
}
func schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"resource": {
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/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"),
},
},
},
},
},
},
Required: []string{"resource"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"},
}
}
func schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"operatorStates": {
SchemaProps: spec.SchemaProps{
Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"),
},
},
},
},
},
"additionalFields": {
SchemaProps: spec.SchemaProps{
Description: "additionalFields is reserved for future use",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"},
}
}
func schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"lastEvaluation": {
SchemaProps: spec.SchemaProps{
Description: "lastEvaluation is the ResourceVersion last evaluated",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"state": {
SchemaProps: spec.SchemaProps{
Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"descriptiveState": {
SchemaProps: spec.SchemaProps{
Description: "descriptiveState is an optional more descriptive state field which has no requirements on format",
Type: []string{"string"},
Format: "",
},
},
"details": {
SchemaProps: spec.SchemaProps{
Description: "details contains any extra information that is operator-specific",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Format: "",
},
},
},
},
},
},
Required: []string{"lastEvaluation", "state"},
},
},
}
}
@@ -0,0 +1,4 @@
API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesNavbarPreference,BookmarkUrls
API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsSpec,Resource
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesList,ListMeta
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsList,ListMeta
@@ -20,6 +20,9 @@ var (
rawSchemaPreferencesv1alpha1 = []byte(`{"spec":{"properties":{"cookiePreferences":{"description":"Cookie preferences","properties":{"analytics":{"type":"object"},"functional":{"type":"object"},"performance":{"type":"object"}},"type":"object"},"homeDashboardUID":{"description":"UID for the home dashboard","type":"string"},"language":{"description":"Selected language (beta)","type":"string"},"navbar":{"description":"Navigation preferences","properties":{"bookmarkUrls":{"items":{"type":"string"},"type":"array"}},"required":["bookmarkUrls"],"type":"object"},"queryHistory":{"description":"Explore query history preferences","properties":{"homeTab":{"description":"one of: '' | 'query' | 'starred';","type":"string"}},"type":"object"},"regionalFormat":{"description":"Selected locale (beta)","type":"string"},"theme":{"description":"light, dark, empty is default","type":"string"},"timezone":{"description":"The timezone selection\nTODO: this should use the timezone defined in common","type":"string"},"weekStart":{"description":"day of the week (sunday, monday, etc)","type":"string"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
versionSchemaPreferencesv1alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaPreferencesv1alpha1, &versionSchemaPreferencesv1alpha1)
rawSchemaStarsv1alpha1 = []byte(`{"spec":{"properties":{"resource":{"items":{"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"type":"array"}},"required":["resource"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
versionSchemaStarsv1alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1)
)
var appManifestData = app.ManifestData{
@@ -35,7 +38,31 @@ var appManifestData = app.ManifestData{
Plural: "Preferences",
Scope: "Namespaced",
Conversion: false,
Schema: &versionSchemaPreferencesv1alpha1,
Admission: &app.AdmissionCapabilities{
Validation: &app.ValidationCapability{
Operations: []app.AdmissionOperation{
app.AdmissionOperationCreate,
app.AdmissionOperationUpdate,
},
},
},
Schema: &versionSchemaPreferencesv1alpha1,
},
{
Kind: "Stars",
Plural: "Stars",
Scope: "Namespaced",
Conversion: false,
Admission: &app.AdmissionCapabilities{
Validation: &app.ValidationCapability{
Operations: []app.AdmissionOperation{
app.AdmissionOperationCreate,
app.AdmissionOperationUpdate,
},
},
},
Schema: &versionSchemaStarsv1alpha1,
},
},
},
@@ -52,6 +79,7 @@ func RemoteManifest() app.Manifest {
var kindVersionToGoType = map[string]resource.Kind{
"Preferences/v1alpha1": v1alpha1.PreferencesKind(),
"Stars/v1alpha1": v1alpha1.StarsKind(),
}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
+1 -1
View File
@@ -9,6 +9,7 @@ require (
cloud.google.com/go/storage v1.55.0 // @grafana/grafana-backend-group
connectrpc.com/connect v1.18.1 // @grafana/observability-traces-and-profiling
cuelang.org/go v0.11.1 // @grafana/grafana-as-code
dario.cat/mergo v1.0.2 // @grafana/grafana-app-platform-squad
filippo.io/age v1.2.1 // @grafana/identity-access-team
github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 // @grafana/grafana-search-and-storage
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // @grafana/partner-datasources
@@ -285,7 +286,6 @@ require (
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
+2 -2
View File
@@ -634,8 +634,8 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSB
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
+1
View File
@@ -506,6 +506,7 @@ contrib.go.opencensus.io/exporter/zipkin v0.1.2 h1:YqE293IZrKtqPnpwDPH/lOqTWD/s3
contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj3gsxcuytSqtH0dxSWW1RE=
contrib.go.opencensus.io/integrations/ocsql v0.1.7 h1:G3k7C0/W44zcqkpRSFyjU9f6HZkbwIrL//qqnlqWZ60=
contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho=
git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38=
+1
View File
@@ -89,6 +89,7 @@ grafana::codegen:run pkg/aggregator
grafana::codegen:run apps/dashboard/pkg
grafana::codegen:run apps/provisioning/pkg
grafana::codegen:run apps/folder/pkg
grafana::codegen:run apps/preferences/pkg
if [ -d "pkg/extensions/apis" ]; then
grafana::codegen:run pkg/extensions
+2
View File
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
"github.com/grafana/grafana/pkg/registry/apis/preferences"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/secret"
@@ -27,6 +28,7 @@ func ProvideRegistryServiceSink(
_ *iam.IdentityAccessManagementAPIBuilder,
_ *query.QueryAPIBuilder,
_ *userstorage.UserStorageAPIBuilder,
_ *preferences.APIBuilder,
_ *provisioning.APIBuilder,
_ *ofrep.APIBuilder,
_ *secret.DependencyRegisterer,
@@ -0,0 +1,60 @@
package preferences
import (
"context"
"slices"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
)
func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return authorizer.DecisionDeny, "valid user is required", err
}
if !attr.IsResourceRequest() || user.GetIsGrafanaAdmin() || attr.GetName() == "" {
return authorizer.DecisionAllow, "", nil
}
name, found := utils.ParseOwnerFromName(attr.GetName())
if !found {
return authorizer.DecisionDeny, "invalid name", nil
}
if attr.GetResource() == "stars" && name.Owner != utils.UserResourceOwner {
return authorizer.DecisionDeny, "stars only support users", nil
}
switch name.Owner {
case utils.NamespaceResourceOwner:
return authorizer.DecisionAllow, "", nil
case utils.UserResourceOwner:
if user.GetUID() == name.Name {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "you may only fetch your own preferences", nil
case utils.TeamResourceOwner:
admin := !attr.IsReadOnly() // we need admin to for non read only commands
teams, err := b.sql.GetTeams(ctx, user.GetOrgID(), user.GetUID(), admin)
if err != nil {
return authorizer.DecisionDeny, "error fetching teams", err
}
if slices.Contains(teams, name.Name) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "not a team member", nil
default:
}
return authorizer.DecisionDeny, "invalid name", nil
})
}
+132
View File
@@ -0,0 +1,132 @@
package preferences
import (
"encoding/json"
"net/http"
"dario.cat/mergo"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apis/preferences/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errhttp"
)
type calculator struct {
defaults preferences.PreferencesSpec
sql *legacy.LegacySQL
}
func newCalculator(cfg *setting.Cfg, sql *legacy.LegacySQL) *calculator {
return &calculator{
sql: sql,
defaults: preferences.PreferencesSpec{
Theme: &cfg.DefaultTheme,
Timezone: &cfg.DateFormats.DefaultTimezone,
WeekStart: &cfg.DateFormats.DefaultWeekStart,
Language: &cfg.DefaultLanguage,
},
}
}
func (s *calculator) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *builder.APIRoutes {
schema := defs["github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preference"].Schema
return &builder.APIRoutes{
Namespace: []builder.APIRouteHandler{
{
Path: "current", // calculate?
Spec: &spec3.PathProps{
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
OperationId: "currentPreferences",
Tags: []string{"Preferences"},
Description: "Get preferences for requester. This combines the user preferences with the team and global defaults",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "namespace",
In: "path",
Required: true,
Example: "default",
Description: "workspace",
Schema: spec.StringProperty(),
},
// Allow getting theme+language from accept
},
},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
StatusCodeResponses: map[int]*spec3.Response{
200: {
ResponseProps: spec3.ResponseProps{
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &schema,
},
},
},
},
},
},
},
},
},
},
},
Handler: s.Current,
},
},
}
}
func (s *calculator) Current(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := identity.GetRequester(ctx)
if err != nil {
errhttp.Write(ctx, err, w)
return
}
ns := user.GetNamespace() // namespace not in context!
list, err := s.sql.ListPreferences(ctx, ns, user, false)
if err != nil {
errhttp.Write(ctx, err, w)
return
}
p, err := merge(s.defaults, list.Items)
if err != nil {
errhttp.Write(ctx, err, w)
return
}
p.Namespace = ns
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(p)
}
// items should be in ascending order of importance
func merge(defaults preferences.PreferencesSpec, items []preferences.Preferences) (*preferences.Preferences, error) {
p := &preferences.Preferences{
TypeMeta: preferences.PreferencesResourceInfo.TypeMeta(),
ObjectMeta: v1.ObjectMeta{
CreationTimestamp: v1.Now(),
},
Spec: defaults,
}
// Iterate in reverse order (least relevant to most relevant)
for _, v := range items {
if err := mergo.Merge(&p.Spec, &v.Spec, mergo.WithOverride); err != nil {
return nil, err
}
}
return p, nil
}
@@ -0,0 +1,56 @@
package preferences
import (
"testing"
"github.com/stretchr/testify/require"
"k8s.io/utils/ptr"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
)
func TestStarsQueries(t *testing.T) {
tests := []struct {
name string
defaults preferences.PreferencesSpec
items []preferences.Preferences
expect preferences.PreferencesSpec
}{
{
name: "test1",
defaults: preferences.PreferencesSpec{
Theme: ptr.To("settings.ini"),
Language: ptr.To("settings.ini"),
HomeDashboardUID: ptr.To("settings.ini"),
Timezone: ptr.To("settings.ini"),
},
items: []preferences.Preferences{
{Spec: preferences.PreferencesSpec{
Theme: ptr.To("namespace"),
Language: ptr.To("namespace"),
HomeDashboardUID: ptr.To("namespace"),
}},
{Spec: preferences.PreferencesSpec{
Theme: ptr.To("team"),
Language: ptr.To("team"),
}},
{Spec: preferences.PreferencesSpec{
Theme: ptr.To("user"),
}},
},
expect: preferences.PreferencesSpec{
Theme: ptr.To("user"),
Language: ptr.To("team"),
HomeDashboardUID: ptr.To("namespace"),
Timezone: ptr.To("settings.ini"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
current, err := merge(tt.defaults, tt.items)
require.NoError(t, err)
require.Equal(t, tt.expect, current.Spec)
})
}
}
@@ -0,0 +1,212 @@
package legacy
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
requestK8s "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
utilsOrig "github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
var (
_ rest.Scoper = (*preferenceStorage)(nil)
_ rest.SingularNameProvider = (*preferenceStorage)(nil)
_ rest.Getter = (*preferenceStorage)(nil)
_ rest.Lister = (*preferenceStorage)(nil)
_ rest.Storage = (*preferenceStorage)(nil)
// _ rest.Creater = (*preferenceStorage)(nil)
// _ rest.Updater = (*preferenceStorage)(nil)
// _ rest.GracefulDeleter = (*preferenceStorage)(nil)
)
func NewPreferencesStorage(namespacer request.NamespaceMapper, sql *LegacySQL) *preferenceStorage {
return &preferenceStorage{
namespacer: namespacer,
sql: sql,
tableConverter: preferences.PreferencesResourceInfo.TableConverter(),
}
}
type preferenceStorage struct {
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
sql *LegacySQL
}
func (s *preferenceStorage) New() runtime.Object {
return preferences.PreferencesKind().ZeroValue()
}
func (s *preferenceStorage) Destroy() {}
func (s *preferenceStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *preferenceStorage) GetSingularName() string {
return strings.ToLower(preferences.PreferencesKind().Kind())
}
func (s *preferenceStorage) NewList() runtime.Object {
return preferences.PreferencesKind().ZeroListValue()
}
func (s *preferenceStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *preferenceStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
ns := requestK8s.NamespaceValue(ctx)
if user.GetIsGrafanaAdmin() {
user = nil // nill user can see everything
}
return s.sql.ListPreferences(ctx, ns, user, true)
}
func (s *preferenceStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
owner, ok := utils.ParseOwnerFromName(name)
if !ok {
return nil, preferences.PreferencesResourceInfo.NewNotFound(name)
}
found, _, err := s.sql.listPreferences(ctx, ns.Value, ns.OrgID, func(req *preferencesQuery) (bool, error) {
switch owner.Owner {
case utils.UserResourceOwner:
if !user.GetIsGrafanaAdmin() && name != user.GetUID() {
return false, fmt.Errorf("you may only fetch your own preferences")
}
req.UserUID = owner.Name
return false, nil
case utils.TeamResourceOwner:
if !user.GetIsGrafanaAdmin() {
teams, err := s.sql.GetTeams(ctx, ns.OrgID, user.GetRawIdentifier(), false)
if err != nil {
return false, err
}
if !slices.Contains(teams, owner.Name) {
return false, fmt.Errorf("you may only fetch teams you belong to")
}
}
req.TeamUID = owner.Name
return false, nil
case utils.NamespaceResourceOwner:
return false, nil
default:
return false, fmt.Errorf("unsupported name")
}
}, func(p *preferenceModel) bool {
return true
})
if err != nil {
return nil, err
}
if len(found) == 1 {
return &found[0], nil
}
return nil, preferences.PreferencesResourceInfo.NewNotFound(name)
}
func asPreferencesResource(ns string, p *preferenceModel) preferences.Preferences {
owner := utils.OwnerReference{}
if p.TeamUID.Valid {
owner.Owner = utils.TeamResourceOwner
owner.Name = p.TeamUID.String
} else if p.UserUID.Valid {
owner.Owner = utils.UserResourceOwner
owner.Name = p.UserUID.String
} else {
owner.Owner = utils.NamespaceResourceOwner
owner.Name = ""
}
obj := preferences.Preferences{
ObjectMeta: metav1.ObjectMeta{
Name: owner.AsName(),
Namespace: ns,
ResourceVersion: strconv.FormatInt(p.Updated.UnixMilli(), 10),
CreationTimestamp: metav1.NewTime(p.Created.UTC()),
},
Spec: preferences.PreferencesSpec{
Theme: asPointer(p.Theme.String),
HomeDashboardUID: asPointer(p.HomeDashboardUID.String),
Timezone: asPointer(p.Timezone.String),
WeekStart: asPointer(p.WeekStart.String),
},
}
if !p.Created.Equal(p.Updated) {
obj.Annotations = map[string]string{
utilsOrig.AnnoKeyUpdatedTimestamp: p.Updated.UTC().Format(time.RFC3339),
}
}
if p.JSONData != nil {
obj.Spec.Language = asPointer(p.JSONData.Language)
obj.Spec.RegionalFormat = asPointer(p.JSONData.RegionalFormat)
if p.JSONData.QueryHistory.HomeTab != "" {
obj.Spec.QueryHistory = &preferences.PreferencesQueryHistoryPreference{
HomeTab: &p.JSONData.QueryHistory.HomeTab,
}
}
if len(p.JSONData.CookiePreferences) > 0 {
// Analytics interface{} `json:"analytics,omitempty"`
// Performance interface{} `json:"performance,omitempty"`
// Functional interface{} `json:"functional,omitempty"`
obj.Spec.CookiePreferences = preferences.NewPreferencesCookiePreferences()
v, ok := p.JSONData.CookiePreferences["analytics"]
if ok {
obj.Spec.CookiePreferences.Analytics = v
}
v, ok = p.JSONData.CookiePreferences["performance"]
if ok {
obj.Spec.CookiePreferences.Performance = v
}
v, ok = p.JSONData.CookiePreferences["functional"]
if ok {
obj.Spec.CookiePreferences.Functional = v
}
}
if len(p.JSONData.Navbar.BookmarkUrls) > 0 {
obj.Spec.Navbar = &preferences.PreferencesNavbarPreference{
BookmarkUrls: p.JSONData.Navbar.BookmarkUrls,
}
}
}
return obj
}
func asPointer(v string) *string {
if v == "" {
return nil
}
return &v
}
@@ -0,0 +1,139 @@
package legacy
import (
"embed"
"fmt"
"text/template"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
// Templates setup.
var (
//go:embed *.sql
sqlTemplatesFS embed.FS
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`))
)
func mustTemplate(filename string) *template.Template {
if t := sqlTemplates.Lookup(filename); t != nil {
return t
}
panic(fmt.Sprintf("template file not found: %s", filename))
}
// Templates.
var (
sqlStarsQuery = mustTemplate("sql_stars_query.sql")
sqlStarsRV = mustTemplate("sql_stars_rv.sql")
sqlPreferencesQuery = mustTemplate("sql_preferences_query.sql")
sqlPreferencesRV = mustTemplate("sql_preferences_rv.sql")
sqlTeams = mustTemplate("sql_teams.sql")
)
type starQuery struct {
sqltemplate.SQLTemplate
OrgID int64 // >= 1 if UserID != ""
UserUID string
StarTable string
UserTable string
}
func (r starQuery) Validate() error {
if r.UserUID != "" && r.OrgID < 1 {
return fmt.Errorf("requests with a userid, must include an orgID")
}
return nil
}
func newStarQueryReq(sql *legacysql.LegacyDatabaseHelper, user string, orgId int64) starQuery {
return starQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
UserUID: user,
OrgID: orgId,
StarTable: sql.Table("star"),
UserTable: sql.Table("user"),
}
}
type preferencesQuery struct {
sqltemplate.SQLTemplate
OrgID int64 // required
UserUID string
UserTeams []string // also requires user UID
TeamUID string
UserTable string
TeamTable string
PreferencesTable string
}
func (r preferencesQuery) CheckTeams() bool {
return r.UserTeams != nil
}
func (r preferencesQuery) HasTeams() bool {
return len(r.UserTeams) > 0
}
func (r preferencesQuery) Validate() error {
if r.OrgID < 1 {
return fmt.Errorf("must include an orgID")
}
if len(r.UserTeams) > 0 && r.UserUID == "" {
return fmt.Errorf("user required when filtering by a set of teams")
}
return nil
}
func newPreferencesQueryReq(sql *legacysql.LegacyDatabaseHelper, orgId int64) preferencesQuery {
return preferencesQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
OrgID: orgId,
PreferencesTable: sql.Table("preferences"),
UserTable: sql.Table("user"),
TeamTable: sql.Table("team"),
}
}
type teamQuery struct {
sqltemplate.SQLTemplate
OrgID int64
UserUID string
IsAdmin bool
TeamMemberTable string
TeamTable string
UserTable string
}
func (r teamQuery) Validate() error {
if r.UserUID != "" && r.OrgID < 1 {
return fmt.Errorf("requests with a userid, must include an orgID")
}
return nil
}
func newTeamsQueryReq(sql *legacysql.LegacyDatabaseHelper, orgId int64, user string, admin bool) teamQuery {
return teamQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
OrgID: orgId,
UserUID: user,
IsAdmin: admin,
TeamMemberTable: sql.Table("team_member"),
TeamTable: sql.Table("team"),
UserTable: sql.Table("user"),
}
}
@@ -0,0 +1,113 @@
package legacy
import (
"testing"
"text/template"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
)
func TestStarsQueries(t *testing.T) {
// prefix tables with grafana
nodb := &legacysql.LegacyDatabaseHelper{
Table: func(n string) string {
return "grafana." + n
},
}
getStarQuery := func(orgId int64, user string) sqltemplate.SQLTemplate {
v := newStarQueryReq(nodb, user, orgId)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
getPreferencesQuery := func(orgId int64, cb func(q *preferencesQuery)) sqltemplate.SQLTemplate {
v := newPreferencesQueryReq(nodb, orgId)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
cb(&v)
return &v
}
getTeamQuery := func(orgId int64, user string, admin bool) sqltemplate.SQLTemplate {
v := newTeamsQueryReq(nodb, orgId, user, admin)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
SQLTemplatesFS: sqlTemplatesFS,
Templates: map[*template.Template][]mocks.TemplateTestCase{
sqlStarsQuery: {
{
Name: "all",
Data: getStarQuery(0, ""),
},
{
Name: "org",
Data: getStarQuery(3, ""),
},
{
Name: "user",
Data: getStarQuery(3, "abc"),
},
},
sqlStarsRV: {
{
Name: "get",
Data: getStarQuery(0, ""),
},
},
sqlPreferencesQuery: {
{
Name: "all",
Data: getPreferencesQuery(1, func(q *preferencesQuery) {}),
},
{
Name: "user-no-teams",
Data: getPreferencesQuery(1, func(q *preferencesQuery) {
q.UserUID = "uuu"
q.UserTeams = []string{}
}),
},
{
Name: "current", // user + user teams
Data: getPreferencesQuery(1, func(q *preferencesQuery) {
q.UserUID = "uuu"
q.UserTeams = []string{"a", "b", "c"}
}),
},
{
Name: "user",
Data: getPreferencesQuery(1, func(q *preferencesQuery) {
q.UserUID = "uuu"
}),
},
{
Name: "team",
Data: getPreferencesQuery(1, func(q *preferencesQuery) {
q.TeamUID = "ttt"
}),
},
},
sqlPreferencesRV: {
{
Name: "get",
Data: getPreferencesQuery(1, func(q *preferencesQuery) {}),
},
},
sqlTeams: {
{
Name: "members",
Data: getTeamQuery(1, "uuu", false),
},
{
Name: "admin",
Data: getTeamQuery(1, "uuu", true),
},
},
},
})
}
+279
View File
@@ -0,0 +1,279 @@
package legacy
import (
"context"
"database/sql"
"fmt"
"slices"
"strconv"
"time"
authlib "github.com/grafana/authlib/types"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
type dashboardStars struct {
OrgID int64
UserUID string
First int64
Last int64
Dashboards []string
}
type preferenceModel struct {
ID int64
OrgID int64
UserUID sql.NullString
TeamUID sql.NullString
JSONData *pref.PreferenceJSONData
HomeDashboardUID sql.NullString
Timezone sql.NullString
Theme sql.NullString
WeekStart sql.NullString
Created time.Time
Updated time.Time
}
type LegacySQL struct {
db legacysql.LegacyDatabaseProvider
startup time.Time
}
func NewLegacySQL(db legacysql.LegacyDatabaseProvider) *LegacySQL {
return &LegacySQL{db: db, startup: time.Now()}
}
// NOTE: this does not support paging -- lets check if that will be a problem in cloud
func (s *LegacySQL) GetStars(ctx context.Context, orgId int64, user string) ([]dashboardStars, int64, error) {
var max sql.NullString
sql, err := s.db(ctx)
if err != nil {
return nil, 0, err
}
req := newStarQueryReq(sql, user, orgId)
q, err := sqltemplate.Execute(sqlStarsQuery, req)
if err != nil {
return nil, 0, fmt.Errorf("execute template %q: %w", sqlStarsQuery.Name(), err)
}
sess := sql.DB.GetSqlxSession()
rows, err := sess.Query(ctx, q, req.GetArgs()...)
defer func() {
if rows != nil {
_ = rows.Close()
}
}()
stars := []dashboardStars{}
current := &dashboardStars{}
var orgID int64
var userUID string
var dashboardUID string
var updated time.Time
for rows.Next() {
err := rows.Scan(&orgID, &userUID, &dashboardUID, &updated)
if err != nil {
return nil, 0, err
}
if orgID != current.OrgID || userUID != current.UserUID {
if current.UserUID != "" {
stars = append(stars, *current)
}
current = &dashboardStars{
OrgID: orgID,
UserUID: userUID,
}
}
ts := updated.UnixMilli()
if ts > current.Last {
current.Last = ts
}
if ts < current.First || current.First == 0 {
current.First = ts
}
current.Dashboards = append(current.Dashboards, dashboardUID)
}
// Add the last value
if current.UserUID != "" {
stars = append(stars, *current)
}
// Find the RV unless it is a user query
if userUID == "" {
req.Reset()
q, err = sqltemplate.Execute(sqlStarsRV, req)
if err != nil {
return nil, 0, fmt.Errorf("execute template %q: %w", sqlPreferencesRV.Name(), err)
}
err = sess.Get(ctx, &max, q)
if err != nil {
return nil, 0, fmt.Errorf("unable to get RV %w", err)
}
if max.Valid && max.String != "" {
fmt.Printf("max RV: %s\n", max.String)
} else {
updated = s.startup
}
}
return stars, updated.UnixMilli(), err
}
// List all defined preferences in an org (valid for admin users only)
func (s *LegacySQL) listPreferences(ctx context.Context,
ns string, orgId int64,
cb func(req *preferencesQuery) (bool, error),
access func(p *preferenceModel) bool,
) ([]preferences.Preferences, int64, error) {
var results []preferences.Preferences
var rv sql.NullTime
var max sql.NullString
sql, err := s.db(ctx)
if err != nil {
return nil, 0, err
}
req := newPreferencesQueryReq(sql, orgId)
needsRV, err := cb(&req)
if err != nil {
return nil, 0, err
}
q, err := sqltemplate.Execute(sqlPreferencesQuery, req)
if err != nil {
return nil, 0, fmt.Errorf("execute template %q: %w", sqlPreferencesQuery.Name(), err)
}
sess := sql.DB.GetSqlxSession()
rows, err := sess.Query(ctx, q, req.GetArgs()...)
defer func() {
if rows != nil {
_ = rows.Close()
}
}()
for rows.Next() {
// SELECT p.id, p.org_id,
// p.json_data,
// p.timezone,
// p.theme,
// p.week_start,
// p.home_dashboard_uid,
// u.uid as user_uid,
// t.uid as team_uid,
// p.created, p.updated
pref := preferenceModel{}
err := rows.Scan(&pref.ID, &pref.OrgID,
&pref.JSONData,
&pref.Timezone,
&pref.Theme,
&pref.WeekStart,
&pref.HomeDashboardUID,
&pref.UserUID, &pref.TeamUID,
&pref.Created, &pref.Updated)
if err != nil {
return nil, 0, err
}
if pref.Updated.After(rv.Time) {
rv.Time = pref.Updated
}
if !access(&pref) {
continue // user does not have access
}
results = append(results, asPreferencesResource(ns, &pref))
}
if needsRV {
req.Reset()
q, err = sqltemplate.Execute(sqlPreferencesRV, req)
if err != nil {
return nil, 0, fmt.Errorf("execute template %q: %w", sqlPreferencesRV.Name(), err)
}
err = sess.Get(ctx, &max, q)
if err != nil {
return nil, 0, fmt.Errorf("unable to get RV %w", err)
}
if max.Valid && max.String != "" {
fmt.Printf("max RV: %s\n", max.String)
} else {
rv.Time = s.startup
}
}
return results, rv.Time.UnixMilli(), err
}
func (s *LegacySQL) ListPreferences(ctx context.Context, ns string, user identity.Requester, needsRV bool) (*preferences.PreferencesList, error) {
if ns == "" {
return nil, fmt.Errorf("namespace is required")
}
info, err := authlib.ParseNamespace(ns)
if err != nil {
return nil, err
}
// when the user is nil, it is actually admin and can see everything
var teams []string
found, rv, err := s.listPreferences(ctx, ns, info.OrgID,
func(req *preferencesQuery) (bool, error) {
if user != nil {
req.UserUID = user.GetRawIdentifier()
teams, err = s.GetTeams(ctx, info.OrgID, req.UserUID, false)
req.UserTeams = teams
}
return needsRV, err
},
func(p *preferenceModel) bool {
if user == nil || user.GetIsGrafanaAdmin() {
return true
}
if p.UserUID.String != "" {
return user.GetRawIdentifier() == p.UserUID.String
}
if p.TeamUID.String != "" {
return slices.Contains(teams, p.TeamUID.String)
}
return true
},
)
if err != nil {
return nil, err
}
list := &preferences.PreferencesList{
Items: found,
}
if rv > 0 {
list.ResourceVersion = strconv.FormatInt(rv, 10)
}
return list, nil
}
func (s *LegacySQL) GetTeams(ctx context.Context, orgId int64, user string, admin bool) ([]string, error) {
sql, err := s.db(ctx)
if err != nil {
return nil, err
}
req := newTeamsQueryReq(sql, orgId, user, admin)
q, err := sqltemplate.Execute(sqlTeams, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlTeams.Name(), err)
}
teams := []string{}
sess := sql.DB.GetSqlxSession()
err = sess.Select(ctx, &teams, q, req.GetArgs()...)
return teams, err
}
@@ -0,0 +1,26 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM {{ .Ident .PreferencesTable }} as p
LEFT JOIN {{ .Ident .UserTable }} as u ON p.user_id = u.id
LEFT JOIN {{ .Ident .TeamTable }} as t ON p.team_id = t.id
WHERE p.org_id = {{ .Arg .OrgID }}
{{ if .TeamUID }}
AND t.uid = {{ .Arg .TeamUID }}
{{ else if .UserUID }}
AND (u.uid = {{ .Arg .UserUID }}
{{ if .CheckTeams }}
{{ if .HasTeams }}
OR t.uid IN ({{ .ArgList .UserTeams }})
{{ end }}
OR p.user_id = 0
{{ end }}
)
{{ end }}
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM {{ .Ident .PreferencesTable }}
@@ -0,0 +1,10 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM {{ .Ident .StarTable }} as s
JOIN {{ .Ident .UserTable }} as u ON s.user_id = u.id
{{ if ge .OrgID 1 }}
WHERE s.org_id = {{ .Arg .OrgID }}
{{ if .UserUID }}
AND u.uid = {{ .Arg .UserUID }}
{{ end }}{{ end }}
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM {{ .Ident .StarTable }}
@@ -0,0 +1,9 @@
SELECT t.uid FROM {{ .Ident .TeamMemberTable }} as tm
JOIN {{ .Ident .UserTable }} as u ON tm.user_id = u.id
JOIN {{ .Ident .TeamTable }} as t ON tm.team_id = t.id
WHERE tm.org_id = {{ .Arg .OrgID }}
AND u.uid = {{ .Arg .UserUID }}
{{ if .IsAdmin }}
AND tm.permission = 4
{{ end }}
ORDER BY t.id
@@ -0,0 +1,130 @@
package legacy
import (
"context"
"fmt"
"strconv"
"strings"
"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"
authlib "github.com/grafana/authlib/types"
dashboardsV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
var (
_ rest.Scoper = (*starsStorage)(nil)
_ rest.SingularNameProvider = (*starsStorage)(nil)
_ rest.Getter = (*starsStorage)(nil)
_ rest.Lister = (*starsStorage)(nil)
_ rest.Storage = (*starsStorage)(nil)
// _ rest.Creater = (*starsStorage)(nil)
// _ rest.Updater = (*starsStorage)(nil)
// _ rest.GracefulDeleter = (*starsStorage)(nil)
)
func NewStarsStorage(namespacer request.NamespaceMapper, sql *LegacySQL) *starsStorage {
return &starsStorage{
namespacer: namespacer,
sql: sql,
tableConverter: preferences.StarsResourceInfo.TableConverter(),
}
}
type starsStorage struct {
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
sql *LegacySQL
}
func (s *starsStorage) New() runtime.Object {
return preferences.StarsKind().ZeroValue()
}
func (s *starsStorage) Destroy() {}
func (s *starsStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *starsStorage) GetSingularName() string {
return strings.ToLower(preferences.StarsKind().Kind())
}
func (s *starsStorage) NewList() runtime.Object {
return preferences.StarsKind().ZeroListValue()
}
func (s *starsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *starsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, false)
if err != nil {
return nil, err
}
if ns.Value == "" {
return nil, fmt.Errorf("cross cluster listing is not supported")
}
list := &preferences.StarsList{}
found, rv, err := s.sql.GetStars(ctx, ns.OrgID, "")
if err != nil {
return nil, err
}
for _, v := range found {
list.Items = append(list.Items, asStarsResource(s.namespacer(v.OrgID), &v))
}
if rv > 0 {
list.ResourceVersion = strconv.FormatInt(rv, 10)
}
return list, nil
}
func (s *starsStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
ut, uid, err := authlib.ParseTypeID(name)
if err != nil {
return nil, fmt.Errorf("invalid name %w", err)
}
if ut != authlib.TypeUser {
return nil, fmt.Errorf("expecting name with prefix: %s", authlib.TypeUser)
}
found, _, err := s.sql.GetStars(ctx, info.OrgID, uid)
if err != nil || len(found) == 0 {
return nil, err
}
obj := asStarsResource(info.Value, &found[0])
return &obj, nil
}
func asStarsResource(ns string, v *dashboardStars) preferences.Stars {
return preferences.Stars{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("user:%s", v.UserUID),
Namespace: ns,
ResourceVersion: strconv.FormatInt(v.Last, 10),
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.First)),
},
Spec: preferences.StarsSpec{
Resource: []preferences.StarsResource{{
Group: dashboardsV1.APIGroup,
Kind: "Dashboard",
Names: v.Dashboards,
}},
},
}
}
@@ -0,0 +1,14 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM `grafana`.`preferences` as p
LEFT JOIN `grafana`.`user` as u ON p.user_id = u.id
LEFT JOIN `grafana`.`team` as t ON p.team_id = t.id
WHERE p.org_id = 1
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,18 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM `grafana`.`preferences` as p
LEFT JOIN `grafana`.`user` as u ON p.user_id = u.id
LEFT JOIN `grafana`.`team` as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
OR t.uid IN ('a', 'b', 'c')
OR p.user_id = 0
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,15 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM `grafana`.`preferences` as p
LEFT JOIN `grafana`.`user` as u ON p.user_id = u.id
LEFT JOIN `grafana`.`team` as t ON p.team_id = t.id
WHERE p.org_id = 1
AND t.uid = 'ttt'
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,17 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM `grafana`.`preferences` as p
LEFT JOIN `grafana`.`user` as u ON p.user_id = u.id
LEFT JOIN `grafana`.`team` as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
OR p.user_id = 0
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,16 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM `grafana`.`preferences` as p
LEFT JOIN `grafana`.`user` as u ON p.user_id = u.id
LEFT JOIN `grafana`.`team` as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM `grafana`.`preferences`
@@ -0,0 +1,5 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM `grafana`.`star` as s
JOIN `grafana`.`user` as u ON s.user_id = u.id
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1,6 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM `grafana`.`star` as s
JOIN `grafana`.`user` as u ON s.user_id = u.id
WHERE s.org_id = 3
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1,7 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM `grafana`.`star` as s
JOIN `grafana`.`user` as u ON s.user_id = u.id
WHERE s.org_id = 3
AND u.uid = 'abc'
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM `grafana`.`star`
@@ -0,0 +1,7 @@
SELECT t.uid FROM `grafana`.`team_member` as tm
JOIN `grafana`.`user` as u ON tm.user_id = u.id
JOIN `grafana`.`team` as t ON tm.team_id = t.id
WHERE tm.org_id = 1
AND u.uid = 'uuu'
AND tm.permission = 4
ORDER BY t.id
@@ -0,0 +1,6 @@
SELECT t.uid FROM `grafana`.`team_member` as tm
JOIN `grafana`.`user` as u ON tm.user_id = u.id
JOIN `grafana`.`team` as t ON tm.team_id = t.id
WHERE tm.org_id = 1
AND u.uid = 'uuu'
ORDER BY t.id
@@ -0,0 +1,14 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,18 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
OR t.uid IN ('a', 'b', 'c')
OR p.user_id = 0
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,15 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND t.uid = 'ttt'
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,17 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
OR p.user_id = 0
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,16 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM "grafana"."preferences"
@@ -0,0 +1,5 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM "grafana"."star" as s
JOIN "grafana"."user" as u ON s.user_id = u.id
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1,6 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM "grafana"."star" as s
JOIN "grafana"."user" as u ON s.user_id = u.id
WHERE s.org_id = 3
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1,7 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM "grafana"."star" as s
JOIN "grafana"."user" as u ON s.user_id = u.id
WHERE s.org_id = 3
AND u.uid = 'abc'
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM "grafana"."star"
@@ -0,0 +1,7 @@
SELECT t.uid FROM "grafana"."team_member" as tm
JOIN "grafana"."user" as u ON tm.user_id = u.id
JOIN "grafana"."team" as t ON tm.team_id = t.id
WHERE tm.org_id = 1
AND u.uid = 'uuu'
AND tm.permission = 4
ORDER BY t.id
@@ -0,0 +1,6 @@
SELECT t.uid FROM "grafana"."team_member" as tm
JOIN "grafana"."user" as u ON tm.user_id = u.id
JOIN "grafana"."team" as t ON tm.team_id = t.id
WHERE tm.org_id = 1
AND u.uid = 'uuu'
ORDER BY t.id
@@ -0,0 +1,14 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,18 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
OR t.uid IN ('a', 'b', 'c')
OR p.user_id = 0
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,15 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND t.uid = 'ttt'
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,17 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
OR p.user_id = 0
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1,16 @@
SELECT p.id, p.org_id,
p.json_data,
p.timezone,
p.theme,
p.week_start,
p.home_dashboard_uid,
u.uid as user_uid,
t.uid as team_uid,
p.created, p.updated
FROM "grafana"."preferences" as p
LEFT JOIN "grafana"."user" as u ON p.user_id = u.id
LEFT JOIN "grafana"."team" as t ON p.team_id = t.id
WHERE p.org_id = 1
AND (u.uid = 'uuu'
)
ORDER BY p.user_id asc, p.team_id asc, p.org_id asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM "grafana"."preferences"
@@ -0,0 +1,5 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM "grafana"."star" as s
JOIN "grafana"."user" as u ON s.user_id = u.id
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1,6 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM "grafana"."star" as s
JOIN "grafana"."user" as u ON s.user_id = u.id
WHERE s.org_id = 3
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1,7 @@
SELECT s.org_id, u.uid as user_uid, s.dashboard_uid, s.updated
FROM "grafana"."star" as s
JOIN "grafana"."user" as u ON s.user_id = u.id
WHERE s.org_id = 3
AND u.uid = 'abc'
ORDER BY
s.org_id asc, s.user_id asc, s.updated asc
@@ -0,0 +1 @@
SELECT MAX(updated) FROM "grafana"."star"
@@ -0,0 +1,7 @@
SELECT t.uid FROM "grafana"."team_member" as tm
JOIN "grafana"."user" as u ON tm.user_id = u.id
JOIN "grafana"."team" as t ON tm.team_id = t.id
WHERE tm.org_id = 1
AND u.uid = 'uuu'
AND tm.permission = 4
ORDER BY t.id
@@ -0,0 +1,6 @@
SELECT t.uid FROM "grafana"."team_member" as tm
JOIN "grafana"."user" as u ON tm.user_id = u.id
JOIN "grafana"."team" as t ON tm.team_id = t.id
WHERE tm.org_id = 1
AND u.uid = 'uuu'
ORDER BY t.id
+101
View File
@@ -0,0 +1,101 @@
package preferences
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/validation/spec"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/registry/apis/preferences/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/featuremgmt"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql"
)
var _ builder.APIGroupBuilder = (*APIBuilder)(nil)
type APIBuilder struct {
namespacer request.NamespaceMapper
sql *legacy.LegacySQL
prefs pref.Service
calculator *calculator // joins all preferences
}
func RegisterAPIService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
db db.DB,
prefs pref.Service,
apiregistration builder.APIRegistrar,
) *APIBuilder {
// Requires development settings and clearly experimental
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil
}
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
builder := &APIBuilder{
prefs: prefs, // for writing
namespacer: request.GetNamespaceMapper(cfg),
sql: sql,
calculator: newCalculator(cfg, sql),
}
apiregistration.RegisterAPI(builder)
return builder
}
// AllowedV0Alpha1Resources implements builder.APIGroupBuilder.
func (b *APIBuilder) AllowedV0Alpha1Resources() []string {
return nil
}
func (b *APIBuilder) GetGroupVersion() schema.GroupVersion {
return preferences.GroupVersion
}
func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error {
gv := preferences.GroupVersion
err := preferences.AddToScheme(scheme)
if err != nil {
return err
}
metav1.AddToGroupVersion(scheme, gv)
return scheme.SetVersionPriority(gv)
}
func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
storage := map[string]rest.Storage{}
stars := preferences.StarsResourceInfo
storage[stars.StoragePath()] = legacy.NewStarsStorage(b.namespacer, b.sql)
prefs := preferences.PreferencesResourceInfo
// Unified storage
// store, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter)
// if err != nil {
// return err
// }
storage[prefs.StoragePath()] = legacy.NewPreferencesStorage(b.namespacer, b.sql)
apiGroupInfo.VersionedResourcesStorageMap[preferences.APIVersion] = storage
return nil
}
func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return preferences.GetOpenAPIDefinitions
}
func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
return b.calculator.GetAPIRoutes(defs)
}
@@ -0,0 +1,44 @@
package utils
import "strings"
// +enum
type ResourceOwner string
const (
// Used when there is a single resource for a given org|stack
NamespaceResourceOwner ResourceOwner = "namespace"
// Resource with a 1:1 user mapping
UserResourceOwner ResourceOwner = "user"
// Resource with a 1:1 team mapping
TeamResourceOwner ResourceOwner = "team"
// The name does not match user, team or namespace
UnknownResourceOwner ResourceOwner = ""
)
type OwnerReference struct {
Owner ResourceOwner // the resource owner
Name string // the team|user name
}
func (o OwnerReference) AsName() string {
if o.Name == "" || o.Owner == NamespaceResourceOwner {
return string(o.Owner)
}
return string(o.Owner) + ":" + o.Name
}
func ParseOwnerFromName(name string) (OwnerReference, bool) {
before, after, found := strings.Cut(name, ":")
if found && len(after) > 0 {
switch before {
case "user":
return OwnerReference{Owner: UserResourceOwner, Name: after}, true
case "team":
return OwnerReference{Owner: TeamResourceOwner, Name: after}, true
}
} else if name == "namespace" {
return OwnerReference{Owner: NamespaceResourceOwner}, true
}
return OwnerReference{}, false
}
@@ -0,0 +1,66 @@
package utils_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
)
func TestLegacyAuthorizer(t *testing.T) {
tests := []struct {
name string
input string
output utils.OwnerReference
found bool
}{
{
name: "invalid",
input: "xxx:yyy",
output: utils.OwnerReference{},
found: false,
},
{
name: "with user",
input: "user:a",
output: utils.OwnerReference{Owner: utils.UserResourceOwner, Name: "a"},
found: true,
},
{
name: "missing user",
input: "user:",
output: utils.OwnerReference{},
found: false,
},
{
name: "with team",
input: "team:b",
output: utils.OwnerReference{Owner: utils.TeamResourceOwner, Name: "b"},
found: true,
},
{
name: "missing team",
input: "team:",
output: utils.OwnerReference{},
found: false,
},
{
name: "for namespace",
input: "namespace",
output: utils.OwnerReference{Owner: utils.NamespaceResourceOwner},
found: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output, found := utils.ParseOwnerFromName(tt.input)
require.Equal(t, tt.output, output)
require.Equal(t, tt.found, found)
if tt.found {
require.Equal(t, tt.input, output.AsName())
}
})
}
}
+2
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
"github.com/grafana/grafana/pkg/registry/apis/preferences"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/secret"
@@ -47,6 +48,7 @@ var WireSet = wire.NewSet(
provisioning.RegisterAPIService,
service.RegisterAPIService,
query.RegisterAPIService,
preferences.RegisterAPIService,
userstorage.RegisterAPIService,
ofrep.RegisterAPIService,
)
+7 -4
View File
@@ -57,6 +57,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
"github.com/grafana/grafana/pkg/registry/apis/preferences"
provisioning2 "github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/extras"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
@@ -802,6 +803,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
return nil, err
}
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, apiserverService)
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles)
webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v3 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder)
@@ -820,7 +822,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, repositoryFactory)
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, repositoryFactory)
if err != nil {
return nil, err
}
@@ -833,7 +835,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, ofrepAPIBuilder, dependencyRegisterer)
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer)
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
if err != nil {
return nil, err
@@ -1383,6 +1385,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
return nil, err
}
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, apiserverService)
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles)
webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, renderingService, resourceClient, eventualRestConfigProvider)
v3 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder)
@@ -1401,7 +1404,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, repositoryFactory)
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, repositoryFactory)
if err != nil {
return nil, err
}
@@ -1414,7 +1417,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, ofrepAPIBuilder, dependencyRegisterer)
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer)
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
if err != nil {
return nil, err
+10 -7
View File
@@ -332,6 +332,8 @@ type OrgUsers struct {
Editor User
Viewer User
OrgID int64
// Separate standalone service accounts with different roles
AdminServiceAccount serviceaccounts.ServiceAccountDTO
AdminServiceAccountToken string
@@ -559,18 +561,19 @@ func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers {
Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil),
Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil),
}
users.OrgID = users.Admin.Identity.GetOrgID()
// Create service accounts
users.AdminServiceAccount = c.CreateServiceAccount(users.Admin, "admin-sa", users.Admin.Identity.GetOrgID(), org.RoleAdmin)
users.AdminServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.AdminServiceAccount.Id, users.Admin.Identity.GetOrgID(), "admin-token", 0)
users.AdminServiceAccount = c.CreateServiceAccount(users.Admin, "admin-sa", users.OrgID, org.RoleAdmin)
users.AdminServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.AdminServiceAccount.Id, users.OrgID, "admin-token", 0)
users.EditorServiceAccount = c.CreateServiceAccount(users.Admin, "editor-sa", users.Admin.Identity.GetOrgID(), org.RoleEditor)
users.EditorServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.EditorServiceAccount.Id, users.Admin.Identity.GetOrgID(), "editor-token", 0)
users.EditorServiceAccount = c.CreateServiceAccount(users.Admin, "editor-sa", users.OrgID, org.RoleEditor)
users.EditorServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.EditorServiceAccount.Id, users.OrgID, "editor-token", 0)
users.ViewerServiceAccount = c.CreateServiceAccount(users.Admin, "viewer-sa", users.Admin.Identity.GetOrgID(), org.RoleViewer)
users.ViewerServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.ViewerServiceAccount.Id, users.Admin.Identity.GetOrgID(), "viewer-token", 0)
users.ViewerServiceAccount = c.CreateServiceAccount(users.Admin, "viewer-sa", users.OrgID, org.RoleViewer)
users.ViewerServiceAccountToken = c.CreateServiceAccountToken(users.Admin, users.ViewerServiceAccount.Id, users.OrgID, "viewer-token", 0)
users.Staff = c.CreateTeam("staff", "staff@"+orgName, users.Admin.Identity.GetOrgID())
users.Staff = c.CreateTeam("staff", "staff@"+orgName, users.OrgID)
// Add Admin and Editor to Staff team as Admin and Member, respectively.
c.AddOrUpdateTeamMember(users.Admin, users.Staff.ID, team.PermissionTypeAdmin)
@@ -0,0 +1,143 @@
package preferences
import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationPreferences(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental APIs
DisableAnonymous: true,
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagMultiTenantFrontend, // So we can compare to boot-data preferences
},
})
t.Run("legacy preferences api", func(t *testing.T) {
ctx := context.Background()
clientAdmin := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
GVR: preferences.PreferencesResourceInfo.GroupVersionResource(),
})
clientViewer := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Viewer,
GVR: preferences.PreferencesResourceInfo.GroupVersionResource(),
})
// List is empty when we start
rsp, err := clientAdmin.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Empty(t, rsp.Items, "no preferences saved yet")
raw := make(map[string]any)
legacyResponse := apis.DoRequest(helper, apis.RequestParams{
User: clientAdmin.Args.User,
Method: http.MethodPut,
Path: "/api/user/preferences",
Body: []byte(`{
"weekStart": "saturday"
}`),
}, &raw)
require.Equal(t, http.StatusOK, legacyResponse.Response.StatusCode, "create preference for user")
// http://localhost:3000/api/teams/1/preferences
legacyResponse = apis.DoRequest(helper, apis.RequestParams{
User: clientAdmin.Args.User,
Method: http.MethodPut,
Path: fmt.Sprintf("/api/teams/%d/preferences", helper.Org1.Staff.ID),
Body: []byte(`{
"weekStart": "sunday",
"timezone": "africa"
}`),
}, &raw)
require.Equal(t, http.StatusOK, legacyResponse.Response.StatusCode, "create preference for user")
// http://localhost:3000/api/org/preferences
legacyResponse = apis.DoRequest(helper, apis.RequestParams{
User: clientAdmin.Args.User,
Method: http.MethodPut,
Path: "/api/org/preferences",
Body: []byte(`{
"weekStart": "sunday",
"timezone": "africa",
"theme": "dark"
}`),
}, &raw)
require.Equal(t, http.StatusOK, legacyResponse.Response.StatusCode, "create preference for user")
// Admin has access to all three (namespace, team, and user)
rsp, err = clientAdmin.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
names := []string{}
for _, item := range rsp.Items {
names = append(names, item.GetName())
}
require.Equal(t, []string{
"namespace",
fmt.Sprintf("team:%s", helper.Org1.Staff.UID),
clientAdmin.Args.User.Identity.GetUID(),
}, names)
// The viewer should only have namespace (eg org level) permissions
rsp, err = clientViewer.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
names = []string{}
for _, item := range rsp.Items {
names = append(names, item.GetName())
}
require.Equal(t, []string{"namespace"}, names)
// Pull the preferences out of bootdata (many other things are included!)
type shim struct {
User preferences.PreferencesSpec `json:"user"` // pretend
}
bootdata := apis.DoRequest(helper, apis.RequestParams{
User: clientAdmin.Args.User,
Method: http.MethodGet,
Path: "/bootdata",
}, &shim{})
require.Equal(t, http.StatusOK, bootdata.Response.StatusCode, "get bootdata preferences")
jj, _ := json.Marshal(bootdata.Result.User)
require.JSONEq(t, `{
"timezone":"africa",
"weekStart":"saturday",
"theme":"dark",
"language":"en-US", `+ // FROM global default!
`"regionalFormat":""
}`, string(jj))
current := apis.DoRequest(helper, apis.RequestParams{
User: clientAdmin.Args.User,
Method: http.MethodGet,
Path: "/apis/preferences.grafana.app/v1alpha1/namespaces/default/current",
}, &preferences.Preferences{})
require.Equal(t, http.StatusOK, current.Response.StatusCode, "get current preferences")
require.Equal(t, "saturday", *current.Result.Spec.WeekStart) // from user
require.Equal(t, "africa", *current.Result.Spec.Timezone) // from team
require.Equal(t, "dark", *current.Result.Spec.Theme) // from org
require.Equal(t, "en-US", *current.Result.Spec.Language) // settings.ini
})
}